]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Salvage Job Board (#37549)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Sun, 18 May 2025 04:02:52 +0000 (00:02 -0400)
committerGitHub <noreply@github.com>
Sun, 18 May 2025 04:02:52 +0000 (14:02 +1000)
* Salvage Job Board

* More development

* Small boy

* Computer yaml (partial)

* UI

* Rank unlock logic

* Job label printing

* appraisal tool integration

* Jobs

* add board to QM locker

* boom!

* command desc

* mild rewording

* ackh, mein pr ist brohken

45 files changed:
Content.Client/Cargo/BUI/CargoOrderConsoleBoundUserInterface.cs
Content.Client/Cargo/UI/CargoConsoleMenu.xaml.cs
Content.Client/Salvage/UI/JobEntry.xaml [new file with mode: 0644]
Content.Client/Salvage/UI/JobEntry.xaml.cs [new file with mode: 0644]
Content.Client/Salvage/UI/SalvageJobBoardBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Salvage/UI/SalvageJobBoardMenu.xaml [new file with mode: 0644]
Content.Client/Salvage/UI/SalvageJobBoardMenu.xaml.cs [new file with mode: 0644]
Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs
Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs
Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
Content.Server/Cargo/Systems/CargoSystem.Orders.cs
Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
Content.Server/Cargo/Systems/PriceGunSystem.cs
Content.Server/Salvage/JobBoard/JobBoardCommands.cs [new file with mode: 0644]
Content.Server/Salvage/JobBoard/JobBoardLabelComponent.cs [new file with mode: 0644]
Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs [new file with mode: 0644]
Content.Server/Salvage/JobBoard/SalvageJobsDataComponent.cs [new file with mode: 0644]
Content.Shared/Cargo/BUI/CargoConsoleInterfaceState.cs
Content.Shared/Cargo/Components/CargoOrderConsoleComponent.cs
Content.Shared/Cargo/Prototypes/CargoBountyGroupPrototype.cs [new file with mode: 0644]
Content.Shared/Cargo/Prototypes/CargoBountyPrototype.cs
Content.Shared/Labels/EntitySystems/LabelSystem.cs
Content.Shared/Paper/PaperSystem.cs
Content.Shared/Salvage/JobBoard/SalvageJobBoardConsoleComponent.cs [new file with mode: 0644]
Resources/Locale/en-US/cargo/bounties.ftl
Resources/Locale/en-US/cargo/price-gun-component.ftl
Resources/Locale/en-US/commands/toolshed-commands.ftl
Resources/Locale/en-US/salvage/job-board.ftl [new file with mode: 0644]
Resources/Prototypes/Catalog/Bounties/bounties.yml
Resources/Prototypes/Catalog/Bounties/groups.yml [new file with mode: 0644]
Resources/Prototypes/Catalog/Bounties/salvage_jobs.yml [new file with mode: 0644]
Resources/Prototypes/Catalog/Cargo/markets.yml
Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
Resources/Prototypes/Entities/Mobs/NPCs/carp.yml
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Objects/Materials/materials.yml
Resources/Prototypes/Entities/Objects/Materials/ore.yml
Resources/Prototypes/Entities/Objects/Materials/scrap.yml
Resources/Prototypes/Entities/Objects/Misc/paper.yml
Resources/Prototypes/Entities/Stations/base.yml
Resources/Prototypes/Entities/Stations/nanotrasen.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/tags.yml
Resources/Textures/Structures/Machines/computers.rsi/meta.json
Resources/Textures/Structures/Machines/computers.rsi/salvjob.png [new file with mode: 0644]

index 52846cefdb9388214cb4363242a8cdefa4c49633..3bd220bfadd91326b9de3988b689981c3a993fae 100644 (file)
@@ -138,6 +138,11 @@ namespace Content.Client.Cargo.BUI
 
             AccountName = cState.Name;
 
+            if (_menu == null)
+                return;
+
+            _menu.ProductCatalogue = cState.Products;
+
             _menu?.UpdateStation(station);
             Populate(cState.Orders);
         }
index 4c729b795b48dfdc23c377f278ec476694988f4b..dfc61c0527183e624217d68397fe5045af05610e 100644 (file)
@@ -40,6 +40,8 @@ namespace Content.Client.Cargo.UI
         private readonly List<string> _categoryStrings = new();
         private string? _category;
 
+        public List<ProtoId<CargoProductPrototype>> ProductCatalogue = new();
+
         public CargoConsoleMenu(EntityUid owner, IEntityManager entMan, IPrototypeManager protoManager, SpriteSystem spriteSystem)
         {
             RobustXamlLoader.Load(this);
@@ -113,14 +115,16 @@ namespace Content.Client.Cargo.UI
             Categories.SelectId(id);
         }
 
-        public IEnumerable<CargoProductPrototype> ProductPrototypes
+        private IEnumerable<CargoProductPrototype> ProductPrototypes
         {
             get
             {
                 var allowedGroups = _entityManager.GetComponentOrNull<CargoOrderConsoleComponent>(_owner)?.AllowedGroups;
 
-                foreach (var cargoPrototype in _protoManager.EnumeratePrototypes<CargoProductPrototype>())
+                foreach (var cargoPrototypeId in ProductCatalogue)
                 {
+                    var cargoPrototype = _protoManager.Index(cargoPrototypeId);
+
                     if (!allowedGroups?.Contains(cargoPrototype.Group) ?? false)
                         continue;
 
diff --git a/Content.Client/Salvage/UI/JobEntry.xaml b/Content.Client/Salvage/UI/JobEntry.xaml
new file mode 100644 (file)
index 0000000..bad88f8
--- /dev/null
@@ -0,0 +1,29 @@
+<BoxContainer xmlns="https://spacestation14.io"
+              xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+              Margin="10 10 10 0"
+              HorizontalExpand="True"
+              Visible="True">
+    <PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+            <BoxContainer HorizontalExpand="True" Orientation="Vertical" MaxWidth="380">
+                <RichTextLabel Name="NameLabel" StyleClasses="LabelKeyText"/>
+                <customControls:HSeparator Margin="0 0 0 5"/>
+                <RichTextLabel Name="DescriptionLabel" HorizontalExpand="True"/>
+                <Control MinHeight="10"/>
+                <RichTextLabel Name="ManifestLabel"/>
+                <Control MinHeight="10"/>
+                <RichTextLabel Name="RewardLabel" VerticalExpand="True" VerticalAlignment="Bottom"/>
+            </BoxContainer>
+            <BoxContainer HorizontalExpand="True" VerticalExpand="True" VerticalAlignment="Center">
+                <BoxContainer Orientation="Vertical" HorizontalExpand="True" HorizontalAlignment="Center">
+                    <TextureRect Name="IconRect" MinSize="64 64" HorizontalAlignment="Center"/>
+                    <Control MinHeight="20"/>
+                    <Button Name="PrintButton"
+                            Text="{Loc 'bounty-console-label-button-text'}"
+                            HorizontalExpand="False"
+                            HorizontalAlignment="Center"/>
+                </BoxContainer>
+            </BoxContainer>
+        </BoxContainer>
+    </PanelContainer>
+</BoxContainer>
diff --git a/Content.Client/Salvage/UI/JobEntry.xaml.cs b/Content.Client/Salvage/UI/JobEntry.xaml.cs
new file mode 100644 (file)
index 0000000..ead0270
--- /dev/null
@@ -0,0 +1,44 @@
+using System.Numerics;
+using Content.Client.Message;
+using Content.Shared.Cargo.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Salvage.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class JobEntry : BoxContainer
+{
+    public Action? OnLabelButtonPressed;
+
+    public JobEntry(CargoBountyPrototype job, IEntityManager entMan)
+    {
+        RobustXamlLoader.Load(this);
+
+        NameLabel.SetMarkup(Loc.GetString($"salv-job-board-name-{job.ID}"));
+
+        var items = new List<string>();
+        foreach (var entry in job.Entries)
+        {
+            items.Add(Loc.GetString("bounty-console-manifest-entry",
+                ("amount", entry.Amount),
+                ("item", Loc.GetString(entry.Name))));
+        }
+        ManifestLabel.SetMarkup(Loc.GetString("job-board-ui-label-items", ("item", string.Join(", ", items))));
+        RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", job.Reward)));
+        DescriptionLabel.SetMarkup(Loc.GetString(job.Description));
+
+        if (job.Sprite != null)
+        {
+            var texture = entMan.System<SpriteSystem>().Frame0(job.Sprite);
+
+            // Make sure the actual size of the control is the same regardless of the texture size.
+            IconRect.TextureScale = Vector2.One * (3f / (texture.Size.X / 32f));
+            IconRect.Texture = texture;
+        }
+
+        PrintButton.OnPressed += _ => OnLabelButtonPressed?.Invoke();
+    }
+}
diff --git a/Content.Client/Salvage/UI/SalvageJobBoardBoundUserInterface.cs b/Content.Client/Salvage/UI/SalvageJobBoardBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..e05ea42
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Shared.Cargo.Components;
+using Content.Shared.Salvage.JobBoard;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Salvage.UI;
+
+[UsedImplicitly]
+public sealed class SalvageJobBoardBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+    [ViewVariables]
+    private SalvageJobBoardMenu? _menu;
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = this.CreateWindow<SalvageJobBoardMenu>();
+
+        _menu.OnLabelButtonPressed += id =>
+        {
+            SendMessage(new JobBoardPrintLabelMessage(id));
+        };
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState message)
+    {
+        base.UpdateState(message);
+
+        if (message is not SalvageJobBoardConsoleState state)
+            return;
+
+        _menu?.Update(state);
+    }
+}
diff --git a/Content.Client/Salvage/UI/SalvageJobBoardMenu.xaml b/Content.Client/Salvage/UI/SalvageJobBoardMenu.xaml
new file mode 100644 (file)
index 0000000..46b41e0
--- /dev/null
@@ -0,0 +1,45 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+                      xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+                      xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+                      Title="{Loc 'job-board-ui-window-title'}"
+                      SetSize="550 550"
+                      Resizable="False">
+    <BoxContainer Orientation="Vertical"
+                  VerticalExpand="True"
+                  HorizontalExpand="True">
+        <BoxContainer Orientation="Horizontal" Margin="20 10 20 0">
+            <ProgressBar Name="RankProgressBar" HorizontalExpand="True" MinValue="0" MaxValue="1">
+                <ProgressBar.ForegroundStyleBoxOverride>
+                    <gfx:StyleBoxFlat BackgroundColor="#FFFF00"/>
+                </ProgressBar.ForegroundStyleBoxOverride>
+            </ProgressBar>
+            <Control MinWidth="20"/>
+            <RichTextLabel Text="{Loc 'job-board-ui-label-rank'}" Margin="0 0 5 0"/>
+            <RichTextLabel Text="{Loc 'salvage-job-rank-title-0'}" Name="RankLabel"/>
+        </BoxContainer>
+        <PanelContainer VerticalExpand="True" HorizontalExpand="True" Margin="10">
+            <PanelContainer.PanelOverride>
+                <gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
+            </PanelContainer.PanelOverride>
+            <ScrollContainer HScrollEnabled="False"
+                             HorizontalExpand="True"
+                             VerticalExpand="True">
+                <BoxContainer Name="CurrentJobContainer"
+                              Orientation="Vertical"
+                              VerticalExpand="True"
+                              HorizontalExpand="True"
+                              Margin="0 0 0 10"/>
+            </ScrollContainer>
+        </PanelContainer>
+        <!-- Footer -->
+        <BoxContainer Orientation="Vertical">
+            <PanelContainer StyleClasses="LowDivider" />
+            <BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
+                <Label Text="{Loc 'bounty-console-flavor-right'}" StyleClasses="WindowFooterText"
+                       HorizontalAlignment="Right" HorizontalExpand="True"  Margin="0 0 5 0" />
+                <TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
+                             VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
+            </BoxContainer>
+        </BoxContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Salvage/UI/SalvageJobBoardMenu.xaml.cs b/Content.Client/Salvage/UI/SalvageJobBoardMenu.xaml.cs
new file mode 100644 (file)
index 0000000..73fb4bb
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Client.Message;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Salvage.JobBoard;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Salvage.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class SalvageJobBoardMenu : FancyWindow
+{
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+    public Action<string>? OnLabelButtonPressed;
+
+    public SalvageJobBoardMenu()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+    }
+
+    public void Update(SalvageJobBoardConsoleState state)
+    {
+        RankLabel.SetMarkup(Loc.GetString(state.Title));
+        RankProgressBar.Value = state.Progression;
+
+        CurrentJobContainer.Children.Clear();
+        foreach (var job in state.AvailableJobs)
+        {
+            var entry = new JobEntry(_prototypeManager.Index(job), _entityManager);
+            entry.OnLabelButtonPressed += () => OnLabelButtonPressed?.Invoke(job);
+
+            CurrentJobContainer.AddChild(entry);
+        }
+    }
+}
index c650438b2861c6d8e38502f96b381f15b8f7da55..8036f306b35946afae4f4ae575f4de48814f85ef 100644 (file)
@@ -1,4 +1,6 @@
 using Content.Shared.Cargo;
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Server.Cargo.Components;
@@ -41,6 +43,12 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
     [DataField]
     public HashSet<string> CheckedBounties = new();
 
+    /// <summary>
+    /// The group that bounties are pulled from.
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoBountyGroupPrototype> Group = "StationBounty";
+
     /// <summary>
     /// The time at which players will be able to skip the next bounty.
     /// </summary>
index 36db3b83f2dbf0cdac518b6bbe9f506ed48a92db..37d0f5b7d1c3b786897df39994ecaefa7cff1634 100644 (file)
@@ -31,6 +31,16 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component
     [ViewVariables]
     public int NumOrdersCreated;
 
+    /// <summary>
+    /// An all encompassing determiner of what markets can be ordered from.
+    /// Not every console can order from every market, but a console can't order from a market not on this list.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<CargoMarketPrototype>> Markets = new()
+    {
+        "market",
+    };
+
     // TODO: Can probably dump this
     /// <summary>
     /// The cargo shuttle assigned to this station.
index 7cb09b8725c30c0f8af4d3eb979efcbff4615cfa..456d979959869b1caa30669f28197d10c0947aab 100644 (file)
@@ -16,6 +16,7 @@ using Content.Shared.Whitelist;
 using JetBrains.Annotations;
 using Robust.Server.Containers;
 using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
 using Robust.Shared.Utility;
@@ -292,6 +293,13 @@ public sealed partial class CargoSystem
         return IsBountyComplete(container, proto.Entries);
     }
 
+    public bool IsBountyComplete(EntityUid container, ProtoId<CargoBountyPrototype> prototypeId)
+    {
+        var prototype = _protoMan.Index(prototypeId);
+
+        return IsBountyComplete(container, prototype.Entries);
+    }
+
     public bool IsBountyComplete(EntityUid container, CargoBountyPrototype prototype)
     {
         return IsBountyComplete(container, prototype.Entries);
@@ -392,7 +400,9 @@ public sealed partial class CargoSystem
             return false;
 
         // todo: consider making the cargo bounties weighted.
-        var allBounties = _protoMan.EnumeratePrototypes<CargoBountyPrototype>().ToList();
+        var allBounties = _protoMan.EnumeratePrototypes<CargoBountyPrototype>()
+            .Where(p => p.Group == component.Group)
+            .ToList();
         var filteredBounties = new List<CargoBountyPrototype>();
         foreach (var proto in allBounties)
         {
index cf6c7e995595e8e6f28fcea455d1e70f0dafbca5..e6248f177296c54696460f12e46d1ae4018106eb 100644 (file)
@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 using Content.Server.Cargo.Components;
 using Content.Server.Station.Components;
 using Content.Shared.Cargo;
@@ -372,7 +373,7 @@ namespace Content.Server.Cargo.Systems
                 return;
             }
 
-            if (!component.AllowedGroups.Contains(product.Group))
+            if (!GetAvailableProducts((uid, component)).Contains(args.CargoProductId))
                 return;
 
             if (component.SlipPrinter)
@@ -421,7 +422,8 @@ namespace Content.Server.Cargo.Systems
                     GetOutstandingOrderCount(orderDatabase, console.Account),
                     orderDatabase.Capacity,
                     GetNetEntity(station.Value),
-                    orderDatabase.Orders[console.Account]
+                    orderDatabase.Orders[console.Account],
+                    GetAvailableProducts((consoleUid, console))
                 ));
             }
         }
@@ -617,6 +619,29 @@ namespace Content.Server.Cargo.Systems
 
         }
 
+        public List<ProtoId<CargoProductPrototype>> GetAvailableProducts(Entity<CargoOrderConsoleComponent> ent)
+        {
+            if (_station.GetOwningStation(ent) is not { } station ||
+                !TryComp<StationCargoOrderDatabaseComponent>(station, out var db))
+            {
+                return new List<ProtoId<CargoProductPrototype>>();
+            }
+
+            var products = new List<ProtoId<CargoProductPrototype>>();
+
+            // Note that a market must be both on the station and on the console to be available.
+            var markets = ent.Comp.AllowedGroups.Intersect(db.Markets).ToList();
+            foreach (var product in _protoMan.EnumeratePrototypes<CargoProductPrototype>())
+            {
+                if (!markets.Contains(product.Group))
+                    continue;
+
+                products.Add(product.ID);
+            }
+
+            return products;
+        }
+
         #region Station
 
         private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)
index 4fc2c088139d8bef58ea9613e29daf31eb0a2420..351451123ce4e1e9236a9471c91ede4a9e0b314c 100644 (file)
@@ -131,14 +131,14 @@ public sealed partial class CargoSystem
 
     #region Station
 
-    private bool SellPallets(EntityUid gridUid, out HashSet<(EntityUid, OverrideSellComponent?, double)> goods)
+    private bool SellPallets(EntityUid gridUid, EntityUid station, out HashSet<(EntityUid, OverrideSellComponent?, double)> goods)
     {
         GetPalletGoods(gridUid, out var toSell, out goods);
 
         if (toSell.Count == 0)
             return false;
 
-        var ev = new EntitySoldEvent(toSell);
+        var ev = new EntitySoldEvent(toSell, station);
         RaiseLocalEvent(ref ev);
 
         foreach (var ent in toSell)
@@ -230,7 +230,7 @@ public sealed partial class CargoSystem
             return;
         }
 
-        if (!SellPallets(gridUid, out var goods))
+        if (!SellPallets(gridUid, station, out var goods))
             return;
 
         var baseDistribution = CreateAccountDistribution((station, bankAccount));
@@ -267,4 +267,4 @@ public sealed partial class CargoSystem
 /// deleted but after the price has been calculated.
 /// </summary>
 [ByRefEvent]
-public readonly record struct EntitySoldEvent(HashSet<EntityUid> Sold);
+public readonly record struct EntitySoldEvent(HashSet<EntityUid> Sold, EntityUid Station);
index 5e7ab2306068750fb30bf27f67441ddbdba38111..a119595080ef6c289ee36f07bcc1514c6d281d74 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Server.Popups;
+using Content.Server.Salvage.JobBoard;
 using Content.Shared.Cargo.Components;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Timing;
@@ -13,6 +14,7 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
     [Dependency] private readonly PricingSystem _pricingSystem = default!;
     [Dependency] private readonly PopupSystem _popupSystem = default!;
     [Dependency] private readonly CargoSystem _bountySystem = default!;
+    [Dependency] private readonly SalvageJobBoardSystem _salvageJobBoard = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
 
     protected override bool GetPriceOrBounty(Entity<PriceGunComponent> entity, EntityUid target, EntityUid user)
@@ -24,6 +26,10 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
         {
             _popupSystem.PopupEntity(Loc.GetString("price-gun-bounty-complete"), user, user);
         }
+        else if (_salvageJobBoard.FulfillsSalvageJob(target, null, out _))
+        {
+            _popupSystem.PopupEntity(Loc.GetString("price-gun-salvjob-complete"), user, user);
+        }
         else // Otherwise appraise the price
         {
             var price = _pricingSystem.GetPrice(target);
diff --git a/Content.Server/Salvage/JobBoard/JobBoardCommands.cs b/Content.Server/Salvage/JobBoard/JobBoardCommands.cs
new file mode 100644 (file)
index 0000000..f05bf11
--- /dev/null
@@ -0,0 +1,22 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Toolshed;
+
+namespace Content.Server.Salvage.JobBoard;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
+public sealed class JobBoardCommand : ToolshedCommand
+{
+    /// <summary> Completes a bounty automatically. </summary>
+    [CommandImplementation("completeJob")]
+    public void CompleteJob([PipedArgument] EntityUid station, ProtoId<CargoBountyPrototype> job)
+    {
+        if (!TryComp<SalvageJobsDataComponent>(station, out var salvageJobData))
+            return;
+
+        var sys = EntityManager.System<SalvageJobBoardSystem>();
+        sys.TryCompleteSalvageJob((station, salvageJobData), job);
+    }
+}
diff --git a/Content.Server/Salvage/JobBoard/JobBoardLabelComponent.cs b/Content.Server/Salvage/JobBoard/JobBoardLabelComponent.cs
new file mode 100644 (file)
index 0000000..c588a0d
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Salvage.JobBoard;
+
+/// <summary>
+/// Marks a label for a bounty for a given salvage job board prototype.
+/// </summary>
+[RegisterComponent]
+public sealed partial class JobBoardLabelComponent : Component
+{
+    /// <summary>
+    /// The bounty corresponding to this label.
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoBountyPrototype>? JobId;
+}
diff --git a/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs b/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs
new file mode 100644 (file)
index 0000000..993227e
--- /dev/null
@@ -0,0 +1,294 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Cargo.Components;
+using Content.Server.Cargo.Systems;
+using Content.Server.Radio.EntitySystems;
+using Content.Server.Station.Systems;
+using Content.Shared.Cargo.Components;
+using Content.Shared.Cargo.Prototypes;
+using Content.Shared.Labels.EntitySystems;
+using Content.Shared.Paper;
+using Content.Shared.Radio;
+using Content.Shared.Salvage.JobBoard;
+using Robust.Server.Audio;
+using Robust.Server.GameObjects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Salvage.JobBoard;
+
+public sealed class SalvageJobBoardSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly AudioSystem _audio = default!;
+    [Dependency] private readonly CargoSystem _cargo = default!;
+    [Dependency] private readonly LabelSystem _label = default!;
+    [Dependency] private readonly PaperSystem _paper = default!;
+    [Dependency] private readonly RadioSystem _radio = default!;
+    [Dependency] private readonly StationSystem _station = default!;
+    [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+    /// <summary>
+    /// Radio channel that unlock messages are broadcast on.
+    /// </summary>
+    private static readonly ProtoId<RadioChannelPrototype> UnlockChannel = "Supply";
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<EntitySoldEvent>(OnSold);
+        SubscribeLocalEvent<SalvageJobBoardConsoleComponent, BoundUIOpenedEvent>(OnBUIOpened);
+        Subs.BuiEvents<SalvageJobBoardConsoleComponent>(SalvageJobBoardUiKey.Key,
+            subs =>
+            {
+                subs.Event<JobBoardPrintLabelMessage>(OnPrintLabelMessage);
+            });
+    }
+
+    private void OnSold(ref EntitySoldEvent args)
+    {
+        if (!TryComp<SalvageJobsDataComponent>(args.Station, out var salvageJobsData))
+            return;
+
+        foreach (var sold in args.Sold)
+        {
+            if (!FulfillsSalvageJob(sold, (args.Station, salvageJobsData), out var jobId))
+                return;
+            TryCompleteSalvageJob((args.Station, salvageJobsData), jobId.Value);
+        }
+    }
+
+    /// <summary>
+    /// Gets the jobs that the station can currently access.
+    /// </summary>
+    public List<ProtoId<CargoBountyPrototype>> GetAvailableJobs(Entity<SalvageJobsDataComponent> ent)
+    {
+        var outJobs = new List<ProtoId<CargoBountyPrototype>>();
+        var availableGroups = new HashSet<ProtoId<CargoBountyGroupPrototype>>();
+
+        var completedCount = ent.Comp.CompletedJobs.Count;
+        foreach (var (thresholds, rank) in ent.Comp.RankThresholds)
+        {
+            if (completedCount < thresholds)
+                continue;
+            if (rank.BountyGroup == null)
+                continue;
+            availableGroups.Add(rank.BountyGroup.Value);
+        }
+
+        foreach (var bounty in _prototypeManager.EnumeratePrototypes<CargoBountyPrototype>())
+        {
+            if (ent.Comp.CompletedJobs.Contains(bounty))
+                continue;
+
+            if (availableGroups.Contains(bounty.Group))
+                outJobs.Add(bounty);
+        }
+
+        return outJobs;
+    }
+
+    /// <summary>
+    /// Gets the "progression" of a rank, expressed as on the range [0, 1]
+    /// </summary>
+    public float GetRankProgression(Entity<SalvageJobsDataComponent> ent)
+    {
+        // Need to have at least two of these.
+        if (ent.Comp.RankThresholds.Count <= 1)
+            return 1;
+        var completedCount = ent.Comp.CompletedJobs.Count;
+
+        for (var i = ent.Comp.RankThresholds.Count - 1; i >= 0; i--)
+        {
+            var low = ent.Comp.RankThresholds.Keys.ElementAt(i);
+
+            if (completedCount < low)
+                continue;
+
+            // don't worry abooouuuuut it (it'll be O K !)
+            var high = i != ent.Comp.RankThresholds.Count - 1
+                ? ent.Comp.RankThresholds.Keys.ElementAt(i + 1)
+                :  _prototypeManager.EnumeratePrototypes<CargoBountyPrototype>()
+                .Count(p => ent.Comp.RankThresholds.Values
+                    .Select(r => r.BountyGroup)
+                    .Contains(p.Group));
+
+            return (completedCount - low) / (float)(high - low);
+        }
+
+        return 1f;
+    }
+
+    /// <summary>
+    /// Checks if the current station is the max rank
+    /// </summary>
+    public bool IsMaxRank(Entity<SalvageJobsDataComponent> ent)
+    {
+        return GetAvailableJobs(ent).Count == 0;
+    }
+
+    /// <summary>
+    /// Gets the current rank of the station
+    /// </summary>
+    public SalvageRankDatum GetRank(Entity<SalvageJobsDataComponent> ent)
+    {
+        if (IsMaxRank(ent))
+            return ent.Comp.MaxRank;
+        var completedCount = ent.Comp.CompletedJobs.Count;
+
+        foreach (var (threshold, rank) in ent.Comp.RankThresholds.Reverse())
+        {
+            if (completedCount < threshold)
+                continue;
+
+            return rank;
+        }
+        // base case
+        return ent.Comp.RankThresholds[0];
+    }
+
+    /// <summary>
+    ///
+    /// </summary>
+    /// <param name="ent"></param>
+    /// <param name="job"></param>
+    /// <returns></returns>
+    public bool TryCompleteSalvageJob(Entity<SalvageJobsDataComponent> ent, ProtoId<CargoBountyPrototype> job)
+    {
+        if (!GetAvailableJobs(ent).Contains(job))
+            return false;
+
+        var jobProto = _prototypeManager.Index(job);
+
+        var oldRank = GetRank(ent);
+
+        ent.Comp.CompletedJobs.Add(job);
+
+        var newRank = GetRank(ent);
+
+        // Add reward
+        if (TryComp<StationBankAccountComponent>(ent, out var stationBankAccount))
+        {
+            _cargo.UpdateBankAccount(
+                (ent.Owner, stationBankAccount),
+                jobProto.Reward,
+                _cargo.CreateAccountDistribution((ent,  stationBankAccount)));
+        }
+
+        // We ranked up!
+        if (oldRank != newRank)
+        {
+            // We need to find a computer to send the message from.
+            var computerQuery = EntityQueryEnumerator<SalvageJobBoardConsoleComponent>();
+            while (computerQuery.MoveNext(out var uid, out _))
+            {
+                var message = Loc.GetString("job-board-radio-announce", ("rank", FormattedMessage.RemoveMarkupPermissive(Loc.GetString(newRank.Title))));
+                _radio.SendRadioMessage(uid, message, UnlockChannel, uid, false);
+                break;
+            }
+
+            if (newRank.UnlockedMarket is { } market &&
+                TryComp<StationCargoOrderDatabaseComponent>(ent, out var stationCargoOrder))
+            {
+                stationCargoOrder.Markets.Add(market);
+            }
+        }
+
+        var enumerator = EntityQueryEnumerator<SalvageJobBoardConsoleComponent>();
+        while (enumerator.MoveNext(out var consoleUid, out var console))
+        {
+            UpdateUi((consoleUid, console), ent);
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    /// Checks if a given entity fulfills a bounty for the station.
+    /// </summary>
+    public bool FulfillsSalvageJob(EntityUid uid, Entity<SalvageJobsDataComponent>? station, [NotNullWhen(true)] out ProtoId<CargoBountyPrototype>? job)
+    {
+        job = null;
+
+        if (!_label.TryGetLabel<JobBoardLabelComponent>(uid, out var labelEnt))
+            return false;
+
+        if (labelEnt.Value.Comp.JobId is not { } jobId)
+            return false;
+
+        job = jobId;
+
+        if (station is null)
+        {
+            if (_station.GetOwningStation(uid) is not { } stationUid ||
+                !TryComp<SalvageJobsDataComponent>(stationUid, out var stationComp))
+                return false;
+
+            station = (stationUid, stationComp);
+        }
+
+        if (!GetAvailableJobs((station.Value, station.Value.Comp)).Contains(job.Value))
+            return false;
+
+
+        if (!_cargo.IsBountyComplete(uid, job))
+            return false;
+
+        return true;
+    }
+
+    private void OnBUIOpened(Entity<SalvageJobBoardConsoleComponent> ent, ref BoundUIOpenedEvent args)
+    {
+        if (args.UiKey is not SalvageJobBoardUiKey.Key)
+            return;
+
+        if (_station.GetOwningStation(ent.Owner) is not { } station ||
+            !TryComp<SalvageJobsDataComponent>(station, out var jobData))
+            return;
+
+        UpdateUi(ent, (station, jobData));
+    }
+
+    private void OnPrintLabelMessage(Entity<SalvageJobBoardConsoleComponent> ent, ref JobBoardPrintLabelMessage args)
+    {
+        if (_timing.CurTime < ent.Comp.NextPrintTime)
+            return;
+
+        if (_station.GetOwningStation(ent) is not { } station ||
+            !TryComp<SalvageJobsDataComponent>(station, out var jobsData))
+            return;
+
+        if (!_prototypeManager.TryIndex<CargoBountyPrototype>(args.JobId, out var job))
+            return;
+
+        if (!GetAvailableJobs((station, jobsData)).Contains(args.JobId))
+            return;
+
+        _audio.PlayPvs(ent.Comp.PrintSound, ent);
+        var label = SpawnAtPosition(ent.Comp.LabelEntity, Transform(ent).Coordinates);
+        EnsureComp<JobBoardLabelComponent>(label).JobId = job.ID;
+
+        var target = new List<string>();
+        foreach (var entry in job.Entries)
+        {
+            target.Add(Loc.GetString("bounty-console-manifest-entry",
+                ("amount", entry.Amount),
+                ("item", Loc.GetString(entry.Name))));
+        }
+        _paper.SetContent(label, Loc.GetString("job-board-label-text", ("target", string.Join(',', target)), ("reward", job.Reward)));
+
+        ent.Comp.NextPrintTime = _timing.CurTime + ent.Comp.PrintDelay;
+    }
+
+    private void UpdateUi(Entity<SalvageJobBoardConsoleComponent> ent, Entity<SalvageJobsDataComponent> stationEnt)
+    {
+        var state = new SalvageJobBoardConsoleState(
+            GetRank(stationEnt).Title,
+            GetRankProgression(stationEnt),
+            GetAvailableJobs(stationEnt));
+
+        _ui.SetUiState(ent.Owner, SalvageJobBoardUiKey.Key, state);
+    }
+}
diff --git a/Content.Server/Salvage/JobBoard/SalvageJobsDataComponent.cs b/Content.Server/Salvage/JobBoard/SalvageJobsDataComponent.cs
new file mode 100644 (file)
index 0000000..983ee9a
--- /dev/null
@@ -0,0 +1,61 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Salvage.JobBoard;
+
+/// <summary>
+/// holds information for a station relating to the salvage job board
+/// </summary>
+[RegisterComponent]
+[Access(typeof(SalvageJobBoardSystem))]
+public sealed partial class SalvageJobsDataComponent : Component
+{
+    /// <summary>
+    /// A dictionary relating the number of completed jobs needed to the different ranks.
+    /// </summary>
+    [DataField]
+    public SortedDictionary<int, SalvageRankDatum> RankThresholds = new();
+
+    /// <summary>
+    /// The rank given when all salvage jobs are complete.
+    /// </summary>
+    [DataField]
+    public SalvageRankDatum MaxRank;
+
+    /// <summary>
+    /// A list of all completed jobs in order.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<CargoBountyPrototype>> CompletedJobs = new();
+
+    /// <summary>
+    /// Account where rewards are deposited.
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoAccountPrototype> RewardAccount = "Cargo";
+}
+
+/// <summary>
+/// Holds information about salvage job ranks
+/// </summary>
+[DataDefinition]
+public partial record struct SalvageRankDatum
+{
+    /// <summary>
+    /// The title displayed when this rank is reached
+    /// </summary>
+    [DataField]
+    public LocId Title;
+
+    /// <summary>
+    /// The bounties associated with this rank.
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoBountyGroupPrototype>? BountyGroup;
+
+    /// <summary>
+    /// The market that is unlocked when you reach this rank
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoMarketPrototype>? UnlockedMarket;
+}
index 7084477f246ddf3e261822931524bb22d2583f7d..aee1824f932baa36c2c8d8d9e976e672fe13adc3 100644 (file)
@@ -1,3 +1,5 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Cargo.BUI;
@@ -10,13 +12,15 @@ public sealed class CargoConsoleInterfaceState : BoundUserInterfaceState
     public int Capacity;
     public NetEntity Station;
     public List<CargoOrderData> Orders;
+    public List<ProtoId<CargoProductPrototype>> Products;
 
-    public CargoConsoleInterfaceState(string name, int count, int capacity, NetEntity station, List<CargoOrderData> orders)
+    public CargoConsoleInterfaceState(string name, int count, int capacity, NetEntity station, List<CargoOrderData> orders, List<ProtoId<CargoProductPrototype>> products)
     {
         Name = name;
         Count = count;
         Capacity = capacity;
         Station = station;
         Orders = orders;
+        Products = products;
     }
 }
index 44790d8881f28ad1914b407bb4f1a565b1e4ac99..e930fbda230963b386ec28d60037881fb7505f7a 100644 (file)
@@ -78,7 +78,13 @@ public sealed partial class CargoOrderConsoleComponent : Component
     /// All of the <see cref="CargoProductPrototype.Group"/>s that are supported.
     /// </summary>
     [DataField, AutoNetworkedField]
-    public List<ProtoId<CargoMarketPrototype>> AllowedGroups = new() { "market" };
+    public List<ProtoId<CargoMarketPrototype>> AllowedGroups = new()
+    {
+        "market",
+        "SalvageJobReward2",
+        "SalvageJobReward3",
+        "SalvageJobRewardMAX",
+    };
 
     /// <summary>
     /// Access needed to toggle the limit on this console.
diff --git a/Content.Shared/Cargo/Prototypes/CargoBountyGroupPrototype.cs b/Content.Shared/Cargo/Prototypes/CargoBountyGroupPrototype.cs
new file mode 100644 (file)
index 0000000..558d52b
--- /dev/null
@@ -0,0 +1,14 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Cargo.Prototypes;
+
+/// <summary>
+/// Used to categorize bounties for different purposes
+/// </summary>
+[Prototype]
+public sealed partial class CargoBountyGroupPrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; private set; } = default!;
+}
index b40b03672eff55c4fa00ac4db4e5d71a3b7d5725..38ca2286eefbbe161a34a065a74df7ae29360294 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Shared.Whitelist;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Cargo.Prototypes;
 
@@ -39,6 +40,18 @@ public sealed partial class CargoBountyPrototype : IPrototype
     /// </summary>
     [DataField]
     public string IdPrefix = "NT";
+
+    /// <summary>
+    /// A group used for categorizing this bounty.
+    /// </summary>
+    [DataField]
+    public ProtoId<CargoBountyGroupPrototype> Group = "StationBounty";
+
+    /// <summary>
+    /// Optional sprite representing this bounty.
+    /// </summary>
+    [DataField]
+    public SpriteSpecifier? Sprite;
 }
 
 [DataDefinition, Serializable, NetSerializable]
index 569acc7bca6537fc8be011e638ce04f433a1f4b1..f60b690929f6d5f5cffa24d355d4b7a72b573c0a 100644 (file)
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Examine;
 using Content.Shared.Labels.Components;
@@ -141,4 +142,23 @@ public sealed partial class LabelSystem : EntitySystem
         if (TryComp<PaperLabelTypeComponent>(slot.Item, out var type))
             _appearance.SetData(ent, PaperLabelVisuals.LabelType, type.PaperType, ent.Comp2);
     }
+
+    /// <summary>
+    /// Retrieves a label with the specified component from the default label slot.
+    /// </summary>
+    public bool TryGetLabel<T>(Entity<PaperLabelComponent?> ent, [NotNullWhen(true)] out Entity<T>? label) where T : Component
+    {
+        label = null;
+        if (!Resolve(ent, ref ent.Comp, false))
+            return false;
+
+        if (ent.Comp.LabelSlot.Item is not { } labelEnt)
+            return false;
+
+        if (!TryComp<T>(labelEnt, out var labelComp))
+            return false;
+
+        label = (labelEnt, labelComp);
+        return true;
+    }
 }
index 803f973a718a63e710a0a154ffdad68feeb51770..04d6d4026cf63afd4e043c4e164846c6783069ab 100644 (file)
@@ -281,6 +281,12 @@ public sealed class PaperSystem : EntitySystem
         }
     }
 
+    public void SetContent(EntityUid entity, string content)
+    {
+        if (!TryComp<PaperComponent>(entity, out var paper))
+            return;
+        SetContent((entity, paper), content);
+    }
 
     public void SetContent(Entity<PaperComponent> entity, string content)
     {
diff --git a/Content.Shared/Salvage/JobBoard/SalvageJobBoardConsoleComponent.cs b/Content.Shared/Salvage/JobBoard/SalvageJobBoardConsoleComponent.cs
new file mode 100644 (file)
index 0000000..6b1bee2
--- /dev/null
@@ -0,0 +1,72 @@
+using Content.Shared.Cargo.Prototypes;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Salvage.JobBoard;
+
+/// <summary>
+/// Used to view the job board ui
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class SalvageJobBoardConsoleComponent : Component
+{
+    /// <summary>
+    /// A label that this computer can print out.
+    /// </summary>
+    [DataField]
+    public EntProtoId LabelEntity = "PaperSalvageJobLabel";
+
+    /// <summary>
+    /// The sound made when printing occurs
+    /// </summary>
+    [DataField]
+    public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
+
+    /// <summary>
+    /// The time at which the console will be able to print a label again.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan NextPrintTime = TimeSpan.Zero;
+
+    /// <summary>
+    /// The time between prints.
+    /// </summary>
+    [DataField]
+    public TimeSpan PrintDelay = TimeSpan.FromSeconds(5);
+}
+
+[Serializable, NetSerializable]
+public sealed class SalvageJobBoardConsoleState : BoundUserInterfaceState
+{
+    public string Title;
+    public float Progression;
+
+    public List<ProtoId<CargoBountyPrototype>> AvailableJobs;
+
+    public SalvageJobBoardConsoleState(string title, float progression, List<ProtoId<CargoBountyPrototype>> availableJobs)
+    {
+        Title = title;
+        Progression = progression;
+        AvailableJobs = availableJobs;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class JobBoardPrintLabelMessage : BoundUserInterfaceMessage
+{
+    public string JobId;
+
+    public JobBoardPrintLabelMessage(string jobId)
+    {
+        JobId = jobId;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum SalvageJobBoardUiKey : byte
+{
+    Key
+}
index 06012431078b34b91bf721f1ac380832c7c147ca..02eb49250a4ad6720177cfc0857b4a50f787f467 100644 (file)
@@ -11,6 +11,7 @@ bounty-item-clown-shoes = Clown shoes
 bounty-item-corn = Ear of corn
 bounty-item-crayon = Crayon
 bounty-item-cuban-carp = Cuban carp
+bounty-item-diamond = Diamond
 bounty-item-donk-pocket = Donk-pocket
 bounty-item-donut = Donut
 bounty-item-figurine = Action figure
@@ -25,6 +26,11 @@ bounty-item-lime = Lime
 bounty-item-lung = Lung
 bounty-item-monkey-cube = Monkey cube
 bounty-item-mouse = Dead mouse
+bounty-item-ore-bananium = Bananium ore
+bounty-item-ore-gold = Gold ore
+bounty-item-ore-plasma = Plasma ore
+bounty-item-ore-silver = Silver ore
+bounty-item-ore-uranium = Uranium ore
 bounty-item-pancake = Pancake
 bounty-item-pen = Pen
 bounty-item-percussion = Percussion instrument
@@ -32,6 +38,7 @@ bounty-item-pie = Pie
 bounty-item-prison-uniform = Prison uniform
 bounty-item-radio = Radio or Headset
 bounty-item-research-disk = Research disk
+bounty-item-scrap = Scrap
 bounty-item-shiv = Shiv
 bounty-item-soap = Soap
 bounty-item-soup = Soup
@@ -139,8 +146,4 @@ bounty-description-wine = The new librarian and the Quartermaster are falling he
 bounty-description-cotton-boll = A massive swarm of mothroaches ate all the paper and cloth on the station. Send us some cotton to help keep our winged crewmembers fed.
 bounty-description-microwave-machine-board = Mr. Giggles thought it'd be funny to stick forks in all the kitchen microwaves. Help us replace them before the chefs start making clown burgers.
 bounty-description-flashes = GREETINGS \[Station] WE REQUIRE 6 FLASHES DUE TO A NORMAL \[TrainingExercise] WITH SECURITY. EVERYTHING IS \[Normal].
-bounty-description-tooth-space-carp = Some lads from "down unda" need some teeth to make their traditional apparel. Send them a few from some space carp.
-bounty-description-tooth-sharkminnow = The chef is claiming that the teeth of sharkminnows are some kind of high-quality knife. I don't know what they're on about, but they want a set. Send it to them.
 bounty-description-ring = On this EXTRAORDINARY day there will be a wedding between the Gelts, but Mr. Gelt has lost the rings, send a pair of rings.
-bounty-description-remains = TWO. HIVELORD REMAINS, please.
-bounty-description-plates = Our club is interested in trophies of majestic creatures such as the Goliath: send a couple of plates. The reward will come quickly.
index 29a1482d19c0eda424ee9ea54e9c415b5a45f4d5..fe20e3296bdb2a080a8ad4428ce97168dfcec63f 100644 (file)
@@ -2,3 +2,4 @@
 price-gun-verb-text = Appraisal
 price-gun-verb-message = Appraise {THE($object)}.
 price-gun-bounty-complete = The device confirms that the bounty contained within is completed.
+price-gun-salvjob-complete = The device confirms that the salvage job contained within is completed.
index 3a620b049b5e44cafbc8f6704a94e183ada001d7..90c0226ecc0c84d3d46751c60b7a66ba965e988f 100644 (file)
@@ -94,3 +94,5 @@ command-description-xenoartifact-averageResearch =
     Calculates amount of research points average generated xeno artifact will output when fully activated.
 command-description-xenoartifact-unlockAllNodes =
     Unlocks all nodes of artifact.
+command-description-jobboard-completeJob =
+    Completes a given salvage job board job for the station.
diff --git a/Resources/Locale/en-US/salvage/job-board.ftl b/Resources/Locale/en-US/salvage/job-board.ftl
new file mode 100644 (file)
index 0000000..1333be7
--- /dev/null
@@ -0,0 +1,47 @@
+salvage-job-rank-title-0 = [color=gray]Scavenger[/color]
+salvage-job-rank-title-1 = [color=white]Scrapper[/color]
+salvage-job-rank-title-2 = [color=yellow]Specialist[/color]
+salvage-job-rank-title-MAX = [color=gold]Supreme Salvager[/color]
+
+job-board-radio-announce = Salvager rank increased to [bold]{$rank}[/bold]! New orders can be purchased from Cargo.
+
+job-board-ui-window-title = Job Board
+job-board-ui-label-rank = [bold]Rank:[/bold]
+job-board-ui-label-items = Target: [color=red]{$item}[/color]
+
+job-board-label-text = [head=2]Salvage Job Shipment[/head]
+    {"[italic]For use only on official off-station salvage shipments.[/italic]"}
+
+    {"[bold]Target:[/bold]"} {$target}
+    {"[bold]Reward:[/bold]"} ${$reward}
+
+
+    {"[italic]Shipments are subject to inspection by the Donk corporation[/italic]"}
+
+salv-job-board-name-BountyTeethSpaceCarp = Space Carp
+salv-job-board-name-BountySalvageScrap = Deep-Space Debris
+salv-job-board-name-BountySalvageOreGold = Gold (Ore)
+salv-job-board-name-BountySalvageOreSilver = Silver (Ore)
+
+salv-job-board-name-BountySalvageOreUranium = Uranium (Ore)
+salv-job-board-name-BountySalvageOrePlasma = Plasma (Ore)
+salv-job-board-name-BountySalvageOreBananium = Bananium (Ore)
+salv-job-board-name-BountyTeethSharkminnow = Sharkminnow
+
+salv-job-board-name-BountyGoliathPlates = Goliath
+salv-job-board-name-BountyHivelordRemains = Hivelord
+salv-job-board-name-BountySalvageDiamond = Diamond
+
+bounty-description-tooth-space-carp = We need you to get a sample of some space carp teeth. You can find these guys on all kinds of salvage debris. Just be careful about their bite.
+bounty-description-salvage-scrap = We are researching the effects of deep space on station materials, and we need some samples. Find some old junk off of debris and bring it to us.
+bounty-description-salvage-ore-gold = We are engaging in an experimental new electronics manufacturing process. Deliver us a large sum of unrefined gold ore. It can come from any source.
+bounty-description-salvage-ore-silver = We are studying the material effects of silver based on the refining methods. Send us a large amount of unrefined silver ore. It can come from any source.
+
+bounty-description-tooth-sharkminnow = We need you to get a sample of some Sharkminnow teeth. These guys are a fair bit nastier than the smaller carp you're familiar with. Take care to not let them bite you: they'll suck out your blood and heal.
+bounty-description-salvage-ore-plasma = We need a shipment of plasma ore to send over to the research station. Please provide us with some so that we can continue our testing. It can come from any source.
+bounty-description-salvage-ore-uranium = We need a sample of uranium ore for our ongoing experiments on nuclear devices. Be aware that while the uranium does glow slightly, it will probably not harm you. It can come from any source.
+bounty-description-salvage-ore-bananium = We have an ongoing project to decode the mystifying clown genomic sequence. We believe a sample of raw bananium will help us achieve this. Note that this only comes from the rarest of deep-space asteroids.
+
+bounty-description-remains = We need you to get a sample of a few Hivelord cores. Be aware that Hivelords can replicate infinitely if the core is not destroyed. Take care not to get overwhelmed.
+bounty-description-plates = We need you to get a couple sheets of Goliath hide. These guys are pretty slow, but be careful about the tentacles: they'll grab you and pull you to the ground. You don't want to know what happens next.
+bounty-description-diamond = We need you to acquire a few diamonds for some advanced fabrication. These can either be found in the mining asteroid nearby or cut out of the basilisk creature. Whichever way you want to do it, get us some.
index 34be215ffca1ac5cf23dad3801bab0474453f79f..28adfb16acb8afdbee81b80a4ec4788fd05738f0 100644 (file)
       components:
       - Flash
 
-- type: cargoBounty
-  id: BountyTeethSpaceCarp
-  reward: 7500
-  description: bounty-description-tooth-space-carp
-  entries:
-  - name: bounty-item-tooth-space-carp
-    amount: 8
-    whitelist:
-      tags:
-      - ToothSpaceCarp
-
-- type: cargoBounty
-  id: BountyTeethSharkminnow
-  reward: 15000
-  description: bounty-description-tooth-sharkminnow
-  entries:
-  - name: bounty-item-tooth-sharkminnow
-    amount: 5
-    whitelist:
-      tags:
-      - ToothSharkminnow
-
-- type: cargoBounty
-  id: BountyPlates
-  reward: 20000
-  description: bounty-description-plates
-  entries:
-  - name: bounty-item-plates
-    amount: 4
-    whitelist:
-      tags:
-      - GoliathPlate
-
-- type: cargoBounty
-  id: BountyRemains
-  reward: 15000
-  description: bounty-description-remains
-  entries:
-  - name: bounty-item-remains
-    amount: 2
-    whitelist:
-      tags:
-      - HivelordRemains
-
 - type: cargoBounty
   id: BountyRing
   reward: 12500
diff --git a/Resources/Prototypes/Catalog/Bounties/groups.yml b/Resources/Prototypes/Catalog/Bounties/groups.yml
new file mode 100644 (file)
index 0000000..54010b9
--- /dev/null
@@ -0,0 +1,11 @@
+- type: cargoBountyGroup
+  id: StationBounty
+
+- type: cargoBountyGroup
+  id: SalvageJobTier1
+
+- type: cargoBountyGroup
+  id: SalvageJobTier2
+
+- type: cargoBountyGroup
+  id: SalvageJobTier3
diff --git a/Resources/Prototypes/Catalog/Bounties/salvage_jobs.yml b/Resources/Prototypes/Catalog/Bounties/salvage_jobs.yml
new file mode 100644 (file)
index 0000000..83ef2ef
--- /dev/null
@@ -0,0 +1,173 @@
+# NOTE: if you add any bounties to this, you need to go to Resources/Prototypes/Entities/Stations/base.yml and adjust the thresholds on BaseStationSalvageJobs.
+# If you don't do this, you won't raise the limit for completing a given rank and may throw off some balance.
+
+# Tier 1
+
+- type: cargoBounty
+  id: BountyTeethSpaceCarp
+  reward: 7500
+  description: bounty-description-tooth-space-carp
+  group: SalvageJobTier1
+  sprite:
+    sprite: Mobs/Aliens/Carps/space.rsi
+    state: icon
+  entries:
+  - name: bounty-item-tooth-space-carp
+    amount: 10
+    whitelist:
+      tags:
+      - ToothSpaceCarp
+
+- type: cargoBounty
+  id: BountySalvageScrap
+  reward: 7500
+  description: bounty-description-salvage-scrap
+  group: SalvageJobTier1
+  sprite:
+    sprite: Objects/Materials/Scrap/generic.rsi
+    state: metal-1
+  entries:
+  - name: bounty-item-scrap
+    amount: 15
+    whitelist:
+      tags:
+      - SalvageScrap
+
+- type: cargoBounty
+  id: BountySalvageOreGold
+  reward: 7500
+  description: bounty-description-salvage-ore-gold
+  group: SalvageJobTier1
+  sprite:
+    sprite: Objects/Materials/ore.rsi
+    state: gold
+  entries:
+  - name: bounty-item-ore-gold
+    amount: 90
+    whitelist:
+      tags:
+      - OreGold
+
+- type: cargoBounty
+  id: BountySalvageOreSilver
+  reward: 7500
+  description: bounty-description-salvage-ore-silver
+  group: SalvageJobTier1
+  sprite:
+    sprite: Objects/Materials/ore.rsi
+    state: silver
+  entries:
+  - name: bounty-item-ore-silver
+    amount: 90
+    whitelist:
+      tags:
+      - OreSilver
+
+# Tier 2
+
+- type: cargoBounty
+  id: BountyTeethSharkminnow
+  reward: 12500
+  description: bounty-description-tooth-sharkminnow
+  group: SalvageJobTier2
+  sprite:
+    sprite: Mobs/Aliens/Carps/sharkminnow.rsi
+    state: icon
+  entries:
+  - name: bounty-item-tooth-sharkminnow
+    amount: 3
+    whitelist:
+      tags:
+      - ToothSharkminnow
+
+- type: cargoBounty
+  id: BountySalvageOrePlasma
+  reward: 12500
+  description: bounty-description-salvage-ore-plasma
+  group: SalvageJobTier2
+  sprite:
+    sprite: Objects/Materials/ore.rsi
+    state: plasma
+  entries:
+  - name: bounty-item-ore-plasma
+    amount: 45
+    whitelist:
+      tags:
+      - OrePlasma
+
+- type: cargoBounty
+  id: BountySalvageOreUranium
+  reward: 12500
+  description: bounty-description-salvage-ore-uranium
+  group: SalvageJobTier2
+  sprite:
+    sprite: Objects/Materials/ore.rsi
+    state: uranium
+  entries:
+  - name: bounty-item-ore-uranium
+    amount: 45
+    whitelist:
+      tags:
+      - OreUranium
+
+- type: cargoBounty
+  id: BountySalvageOreBananium
+  reward: 12500
+  description: bounty-description-salvage-ore-bananium
+  group: SalvageJobTier2
+  sprite:
+    sprite: Objects/Materials/ore.rsi
+    state: bananium
+  entries:
+  - name: bounty-item-ore-bananium
+    amount: 30
+    whitelist:
+      tags:
+      - OreBananium
+
+# Tier 3
+
+- type: cargoBounty
+  id: BountyGoliathPlates
+  reward: 20000
+  description: bounty-description-plates
+  group: SalvageJobTier3
+  sprite:
+    sprite: Mobs/Aliens/Asteroid/goliath.rsi
+    state: goliath
+  entries:
+  - name: bounty-item-plates
+    amount: 6
+    whitelist:
+      tags:
+      - GoliathPlate
+
+- type: cargoBounty
+  id: BountyHivelordRemains
+  reward: 20000
+  description: bounty-description-remains
+  group: SalvageJobTier3
+  sprite:
+    sprite: Mobs/Aliens/Asteroid/hivelord.rsi
+    state: hivelord
+  entries:
+  - name: bounty-item-remains
+    amount: 3
+    whitelist:
+      tags:
+      - HivelordRemains
+
+- type: cargoBounty
+  id: BountySalvageDiamond
+  reward: 20000
+  description: bounty-description-diamond
+  group: SalvageJobTier3
+  sprite:
+    sprite: Objects/Materials/materials.rsi
+    state: diamond
+  entries:
+  - name: bounty-item-diamond
+    amount: 3
+    whitelist:
+      tags:
+      - Diamond
index b6d8790a8f1ace84f093b5b8e5a596195d1c099e..e1fd3de738f73045f00fcd1ff3622f2e107401f1 100644 (file)
@@ -1,2 +1,11 @@
 - type: cargoMarket
   id: market
+
+- type: cargoMarket
+  id: SalvageJobReward2
+
+- type: cargoMarket
+  id: SalvageJobReward3
+
+- type: cargoMarket
+  id: SalvageJobRewardMAX
index c47fd9769c77abb0281a3a3aa68febfd0d15d0e6..2a15014ce26d07a13698cb2a65d68493ca43753b 100644 (file)
@@ -9,6 +9,7 @@
     - id: CargoSaleComputerCircuitboard
     - id: CargoShuttleConsoleCircuitboard
     - id: SalvageMagnetMachineCircuitboard
+    - id: SalvageJobBoardComputerCircuitboard
     - id: CigPackGreen
       prob: 0.50
     - id: ClothingHeadsetAltCargo
index dbceb9c694f5b13088b4026399ef571e05e72a42..e068b2e9b0b663988e2ae07bbc39f7853d05c02d 100644 (file)
         - id: FoodMeatFish
           amount: 4
         - id: MaterialToothSharkminnow1
-          amount: 1
-          maxAmount: 3
+          amount: 3
     - type: MeleeWeapon
       damage:
         types:
index d61fb43b774adba20bbb1c066a9b0c7206819151..8f2c7f010302d65eb47df952af73177af080ece8 100644 (file)
     prototype: ComputerCargoBounty
   - type: StaticPrice
 
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: SalvageJobBoardComputerCircuitboard
+  name: salvage job board computer board
+  description: A computer printed circuit board for a salvage job board computer.
+  components:
+  - type: Sprite
+    state: cpu_supply
+  - type: ComputerBoard
+    prototype: ComputerSalvageJobBoard
+
 - type: entity
   parent: BaseComputerCircuitboard
   id: SalvageExpeditionsComputerCircuitboard
index b0e3a7e6ed79eda44874474bb335eb955db17eb9..5e04ac55dc85b9e95e2942a8d28e50fd085bd738 100644 (file)
   - type: PhysicalComposition
     materialComposition:
       Diamond: 100
+  - type: Tag
+    tags:
+    - RawMaterial
+    - Diamond
 
 - type: entity
   parent: MaterialDiamond
index 9d07aed853be4dc07095e073894e9f84c17bcc9f..9535eb3dbd37524d59dadd4bd709d3341d8acbeb 100644 (file)
           Quantity: 10
   - type: Item
     heldPrefix: gold
+  - type: Tag
+    tags:
+    - Ore
+    - OreGold
 
 - type: entity
   parent: GoldOre
           Quantity: 10
   - type: Item
     heldPrefix: plasma
+  - type: Tag
+    tags:
+    - Ore
+    - OrePlasma
 
 - type: entity
   parent: PlasmaOre
           Quantity: 10
   - type: Item
     heldPrefix: silver
+  - type: Tag
+    tags:
+    - Ore
+    - OreSilver
 
 - type: entity
   parent: SilverOre
         canReact: false
   - type: Item
     heldPrefix: uranium
+  - type: Tag
+    tags:
+    - Ore
+    - OreUranium
 
 - type: entity
   parent: UraniumOre
           Quantity: 5
   - type: Item
     heldPrefix: bananium
+  - type: Tag
+    tags:
+    - Ore
+    - OreBananium
 
 - type: entity
   parent: BananiumOre
index c4ec20465ee5733c57f3697ff8fc04e3a7f3e049..7e8efe5770650ad83cc2d03f486bd298a55b3d33 100644 (file)
@@ -25,6 +25,7 @@
   - type: Tag
     tags:
     - Recyclable
+    - SalvageScrap
 
 - type: entity
   parent: BaseStructure
index ae7ab0f442b0b2109e35499e3f10e0164e74ca21..7a7127d883918400226bdd61dd4abe8f99a2a7b0 100644 (file)
     - CargoBounties
     - Cargo
 
+- type: entity
+  id: PaperSalvageJobLabel
+  parent: PaperCargoInvoice
+  name: salvage job shipment label
+  description: A paper label designating a crate as containing a shipment to fulfill a salvage job. Selling a crate with this will fulfill the job.
+  components:
+  - type: Sprite
+    layers:
+    - state: paper
+      color: "#f7e574"
+    - state: paper_words
+      map: ["enum.PaperVisualLayers.Writing"]
+      color: "#f7e574"
+      visible: false
+    - state: paper_stamp-generic
+      map: ["enum.PaperVisualLayers.Stamp"]
+      visible: false
+  - type: PaperLabelType
+    paperType: Bounty
+  - type: Tag
+    tags:
+    - Document
+    - Trash
+    - Paper
+  - type: PaperVisuals
+    backgroundImagePath: "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png"
+    contentImagePath: "/Textures/Interface/Paper/paper_content_lined.svg.96dpi.png"
+    backgroundModulate: "#f7e574"
+    contentImageModulate: "#f7e574"
+    backgroundPatchMargin: 16.0, 16.0, 16.0, 16.0
+    contentMargin: 16.0, 16.0, 16.0, 16.0
+  - type: JobBoardLabel
+  - type: StaticPrice #infinitely printable
+    price: 0
+
 - type: entity
   name: character sheet
   parent: Paper
index 513e8827f0a7c52a75076dbb785660181328b2f8..ecd909ff78912a83dd294aefdb6e592ed40179d4 100644 (file)
   components:
     - type: SalvageMagnetData
 
+- type: entity
+  id: BaseStationSalvageJobs
+  abstract: true
+  components:
+  - type: SalvageJobsData
+    rankThresholds:
+      0:
+        title: salvage-job-rank-title-0
+        bountyGroup: SalvageJobTier1
+      3:
+        title: salvage-job-rank-title-1
+        bountyGroup: SalvageJobTier2
+        unlockedMarket: SalvageJobReward2
+      6:
+        title: salvage-job-rank-title-2
+        bountyGroup: SalvageJobTier3
+        unlockedMarket: SalvageJobReward3
+    maxRank:
+      title: salvage-job-rank-title-MAX
+      unlockedMarket: SalvageJobRewardMAX
+
 - type: entity
   id: BaseStationSiliconLawCrewsimov
   abstract: true
index 5fb40f272a2b4cbfd91dde253cff15f1145470db..0b1e143703ceb38d6fc8ed3535a1f532dc319591 100644 (file)
@@ -22,6 +22,7 @@
     - BaseStationAlertLevels
     - BaseStationMagnet
     - BaseStationExpeditions
+    - BaseStationSalvageJobs
     - BaseStationSiliconLawCrewsimov
     - BaseStationAllEventsEligible
     - BaseStationNanotrasen
index 2ade0e46647fde7cb810dcd354f6317c2c54b43a..9420c4d10fd6d0d341794afb208ff2ca3d044f4e 100644 (file)
     guides:
     - Cloning
 
+- type: entity
+  parent: BaseComputerAiAccess
+  id: ComputerSalvageJobBoard
+  name: salvage job board
+  description: Console for accessing salvage jobs, if you're tough enough.
+  components:
+  - type: Sprite
+    layers:
+    - map: ["computerLayerBody"]
+      state: computer
+    - map: ["computerLayerKeyboard"]
+      state: generic_keyboard
+    - map: ["computerLayerScreen"]
+      state: salvjob # givin em a salvjob like hawk tuah
+    - map: ["computerLayerKeys"]
+      state: generic_keys
+    - map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      state: generic_panel_open
+  - type: SalvageJobBoardConsole
+  - type: ActivatableUI
+    key: enum.SalvageJobBoardUiKey.Key
+  - type: UserInterface
+    interfaces:
+      enum.SalvageJobBoardUiKey.Key:
+        type: SalvageJobBoardBoundUserInterface
+      enum.WiresUiKey.Key:
+        type: WiresBoundUserInterface
+  - type: ActiveRadio
+    channels: [ Supply ]
+  - type: Computer
+    board: SalvageJobBoardComputerCircuitboard
+  - type: PointLight
+    radius: 1.5
+    energy: 1.6
+    color: "#b89f25"
+
 - type: entity
   id: ComputerSalvageExpedition
   parent: BaseComputerAiAccess
index 6da7b9ee13ba1cd436e45a161e85cde6f606fa63..04e3be9827b2b27ad7b808d38b8123bac992b4fc 100644 (file)
 - type: Tag
   id: Diagonal
 
+- type: Tag
+  id: Diamond
+
 - type: Tag
   id: Dice
 
 - type: Tag
   id: Ore
 
+- type: Tag
+  id: OreBananium
+
+- type: Tag
+  id: OreGold
+
+- type: Tag
+  id: OrePlasma
+
+- type: Tag
+  id: OreSilver
+
+- type: Tag
+  id: OreUranium
+
 - type: Tag
   id: Packet
 
 - type: Tag
   id: SalvageExperiment
 
+- type: Tag
+  id: SalvageScrap
+
 - type: Tag
   id: Scarf
 
index 0502bd36619d25770cd757fdda11f97b61e77154..65cba1211e69a65cc423088bbfa270ed13aeb6f8 100644 (file)
             "name": "robot",
             "directions": 4
         },
+        {
+            "name": "salvjob",
+            "directions": 4,
+            "delays": [
+                [
+                    1,
+                    0.1,
+                    1,
+                    0.1,
+                    1,
+                    0.1
+                ],
+                [
+                    1,
+                    0.1,
+                    1,
+                    0.1,
+                    1,
+                    0.1
+                ],
+                [
+                    1,
+                    0.1,
+                    1,
+                    0.1,
+                    1,
+                    0.1
+                ],
+                [
+                    1,
+                    0.1,
+                    1,
+                    0.1,
+                    1,
+                    0.1
+                ]
+            ]
+        },
         {
             "name": "security",
             "directions": 4,
diff --git a/Resources/Textures/Structures/Machines/computers.rsi/salvjob.png b/Resources/Textures/Structures/Machines/computers.rsi/salvjob.png
new file mode 100644 (file)
index 0000000..ff8f12b
Binary files /dev/null and b/Resources/Textures/Structures/Machines/computers.rsi/salvjob.png differ