]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Salvage expeditions (#12745)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Thu, 20 Apr 2023 00:43:13 +0000 (10:43 +1000)
committerGitHub <noreply@github.com>
Thu, 20 Apr 2023 00:43:13 +0000 (10:43 +1000)
79 files changed:
Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs
Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml
Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Server/Maps/PlanetCommand.cs
Content.Server/NPC/HTN/HTNSystem.cs
Content.Server/Parallax/BiomeSystem.Commands.cs [new file with mode: 0644]
Content.Server/Parallax/BiomeSystem.cs
Content.Server/Procedural/DungeonJob.Generator.cs
Content.Server/Procedural/DungeonJob.PostGen.cs
Content.Server/Procedural/DungeonJob.cs
Content.Server/Procedural/DungeonSystem.Commands.cs
Content.Server/Procedural/DungeonSystem.cs
Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs [new file with mode: 0644]
Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs [new file with mode: 0644]
Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs [new file with mode: 0644]
Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageSystem.Expeditions.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageSystem.Runner.cs [new file with mode: 0644]
Content.Server/Salvage/SalvageSystem.cs
Content.Server/Salvage/SpawnSalvageMissionJob.cs [new file with mode: 0644]
Content.Server/Shuttles/Events/FTLCompletedEvent.cs
Content.Server/Shuttles/Events/FTLRequestEvent.cs [new file with mode: 0644]
Content.Server/Shuttles/Events/FTLStartedEvent.cs
Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
Content.Shared/Parallax/Biomes/BiomeComponent.cs
Content.Shared/Parallax/Biomes/BiomePrototype.cs [deleted file]
Content.Shared/Parallax/Biomes/BiomeTemplatePrototype.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Layers/BiomeDecalLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Layers/BiomeDummyLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Layers/BiomeEntityLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Layers/IBiomeLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Layers/IBiomeWorldLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs [new file with mode: 0644]
Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs
Content.Shared/Procedural/Dungeon.cs
Content.Shared/Procedural/Loot/BiomeTemplateLoot.cs [new file with mode: 0644]
Content.Shared/Procedural/Loot/ClusterLoot.cs [deleted file]
Content.Shared/Procedural/Loot/DungeonClusterLoot.cs [new file with mode: 0644]
Content.Shared/Procedural/Loot/IDungeonLoot.cs
Content.Shared/Procedural/Loot/SalvageLootPrototype.cs
Content.Shared/Procedural/Rewards/BankReward.cs [deleted file]
Content.Shared/Procedural/Rewards/ISalvageReward.cs [deleted file]
Content.Shared/Procedural/Rewards/SalvageRewardPrototype.cs [deleted file]
Content.Shared/Salvage/Expeditions/Extraction/SalvageExtraction.cs [deleted file]
Content.Shared/Salvage/Expeditions/IFactionExpeditionConfig.cs [deleted file]
Content.Shared/Salvage/Expeditions/Modifiers/ISalvageMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/Modifiers/SalvageBiomeMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/Modifiers/SalvageLightMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/Modifiers/SalvageMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/Modifiers/SalvageTimeMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs [new file with mode: 0644]
Content.Shared/Salvage/Expeditions/SalvageFactionPrototype.cs
Content.Shared/Salvage/Expeditions/Structure/SalvageStructure.cs [deleted file]
Content.Shared/Salvage/Expeditions/Structure/SalvageStructureFaction.cs [deleted file]
Content.Shared/Salvage/ISalvageMission.cs [deleted file]
Content.Shared/Salvage/SalvageExpeditionPrototype.cs [deleted file]
Content.Shared/Salvage/SalvageExpeditions.cs
Content.Shared/Salvage/SharedSalvageSystem.cs
Resources/Locale/en-US/procedural/biome.ftl [new file with mode: 0644]
Resources/Locale/en-US/procedural/expeditions.ftl [new file with mode: 0644]
Resources/Maps/Salvage/medium-1.yml
Resources/Maps/Salvage/small-2.yml
Resources/Prototypes/Catalog/Fills/Paper/salvage_lore.yml [deleted file]
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Specific/xeno.yml
Resources/Prototypes/Procedural/biome_markers.yml [new file with mode: 0644]
Resources/Prototypes/Procedural/biome_ore_templates.yml [new file with mode: 0644]
Resources/Prototypes/Procedural/biome_templates.yml [moved from Resources/Prototypes/biomes.yml with 91% similarity]
Resources/Prototypes/Procedural/dungeon_room_packs.yml
Resources/Prototypes/Procedural/salvage_factions.yml [new file with mode: 0644]
Resources/Prototypes/Procedural/salvage_loot.yml [new file with mode: 0644]
Resources/Prototypes/Procedural/salvage_misc.yml [new file with mode: 0644]
Resources/Prototypes/Procedural/salvage_mods.yml [new file with mode: 0644]

index 67ee82aefbe4d47b53b818371288937f3a192651..ce3e55638496c57a551bbb278ac71483322ff31f 100644 (file)
@@ -128,7 +128,7 @@ namespace Content.Client.Lobby.UI
                     _viewBox.AddChild(viewWest);
                     _viewBox.AddChild(viewEast);
                     _summaryLabel.Text = selectedCharacter.Summary;
-                    EntitySystem.Get<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
+                    _entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
                     GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
                 }
             }
index 30a747d715663dc1346688980eec800d4d1c604d..67280c34f975fd930145ca144e07daa573c4021e 100644 (file)
@@ -1,11 +1,11 @@
 <controls:FancyWindow xmlns="https://spacestation14.io"
                       xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
-                      Title="{Loc 'Salvage expeditions'}"
+                      Title="{Loc 'salvage-expedition-window-title'}"
                       MinSize="800 360">
     <BoxContainer Orientation="Vertical">
         <BoxContainer Orientation="Horizontal">
             <Label Name="NextOfferLabel"
-                   Text="Next offer:"
+                   Text="{Loc 'salvage-expedition-window-next'}"
                    Margin="5"></Label>
             <ProgressBar Name="NextOfferBar"
                          HorizontalExpand="True"
index b312b28f7291769dfb2e9cc40172551239bb2e75..fb4d6de165457fa6792f00dbe372153e6479ea6c 100644 (file)
@@ -1,15 +1,11 @@
+using System.Linq;
 using Content.Client.Computer;
 using Content.Client.Stylesheets;
 using Content.Client.UserInterface.Controls;
 using Content.Shared.Parallax.Biomes;
-using Content.Shared.Procedural;
 using Content.Shared.Procedural.Loot;
-using Content.Shared.Procedural.Rewards;
-using Content.Shared.Random;
-using Content.Shared.Random.Helpers;
 using Content.Shared.Salvage;
-using Content.Shared.Salvage.Expeditions;
-using Content.Shared.Salvage.Expeditions.Structure;
+using Content.Shared.Salvage.Expeditions.Modifiers;
 using Content.Shared.Shuttles.BUIStates;
 using Robust.Client.AutoGenerated;
 using Robust.Client.Graphics;
@@ -31,6 +27,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
 
     public event Action<ushort>? ClaimMission;
     private bool _claimed;
+    private bool _cooldown;
     private TimeSpan _nextOffer;
 
     public SalvageExpeditionWindow()
@@ -44,35 +41,17 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
     public void UpdateState(SalvageExpeditionConsoleState state)
     {
         _claimed = state.Claimed;
+        _cooldown = state.Cooldown;
         _nextOffer = state.NextOffer;
         Container.DisposeAllChildren();
 
         for (var i = 0; i < state.Missions.Count; i++)
         {
-            // TODO: Make this XAML
-            var mission = state.Missions[i];
-            var config = _prototype.Index<SalvageExpeditionPrototype>(mission.Config);
-            var dungeonConfig = _prototype.Index<DungeonConfigPrototype>(config.DungeonConfigPrototype);
-            var faction = SharedSalvageSystem.GetFaction(config.Factions, mission.Seed);
-            var factionConfig = _prototype.Index<SalvageFactionPrototype>(faction);
-
-            // If we ever need this on server then move it
-            var missionDesc = string.Empty;
-            var missionDetails = string.Empty;
-
-            switch (config.Mission)
-            {
-                case SalvageStructure structure:
-                    var structureConfig = (SalvageStructureFaction) factionConfig.Configs[mission.Config];
-                    missionDesc = "Demolition";
-                    // TODO:
-                    missionDetails = $"Destroy {SharedSalvageSystem.GetStructureCount(structure, mission.Seed)} {_prototype.Index<EntityPrototype>(structureConfig.Spawn).Name} structures.";
-                    break;
-                default:
-                    throw new NotImplementedException();
-            }
+            var missionParams = state.Missions[i];
+            var config = missionParams.MissionType;
+            var mission = _salvage.GetMission(missionParams.MissionType, missionParams.Difficulty, missionParams.Seed);
 
-            // Mission
+            // Mission title
             var missionStripe = new StripeBack()
             {
                 Margin = new Thickness(0f, -5f, 0f, 0f)
@@ -80,7 +59,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
 
             missionStripe.AddChild(new Label()
             {
-                Text = missionDesc,
+                Text = Loc.GetString($"salvage-expedition-type-{config.ToString()}"),
                 HorizontalAlignment = HAlignment.Center,
                 Margin = new Thickness(0f, 5f, 0f, 5f),
             });
@@ -94,21 +73,27 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
             // Details
             lBox.AddChild(new Label()
             {
-                Text = $"Difficulty:"
+                Text = Loc.GetString("salvage-expedition-window-difficulty")
             });
 
-            var difficultyColor = StyleNano.NanoGold;
+            Color difficultyColor;
 
-            switch (config.DifficultyRating)
+            switch (missionParams.Difficulty)
             {
                 case DifficultyRating.None:
-                    difficultyColor = StyleNano.ButtonColorDefault;
+                    difficultyColor = Color.FromHex("#52B4E996");
                     break;
                 case DifficultyRating.Minor:
-                    difficultyColor = StyleNano.GoodGreenFore;
+                    difficultyColor = Color.FromHex("#9FED5896");
                     break;
                 case DifficultyRating.Moderate:
-                    difficultyColor = StyleNano.ConcerningOrangeFore;
+                    difficultyColor = Color.FromHex("#EFB34196");
+                    break;
+                case DifficultyRating.Hazardous:
+                    difficultyColor = Color.FromHex("#DE3A3A96");
+                    break;
+                case DifficultyRating.Extreme:
+                    difficultyColor = Color.FromHex("#D381C996");
                     break;
                 default:
                     throw new ArgumentOutOfRangeException();
@@ -116,21 +101,23 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
 
             lBox.AddChild(new Label
             {
-                Text = config.DifficultyRating.ToString(),
+                Text = Loc.GetString($"salvage-expedition-difficulty-{missionParams.Difficulty.ToString()}"),
                 FontColorOverride = difficultyColor,
                 HorizontalAlignment = HAlignment.Left,
                 Margin = new Thickness(0f, 0f, 0f, 5f),
             });
 
             // Details
+            var details = _salvage.GetMissionDescription(mission);
+
             lBox.AddChild(new Label
             {
-                Text = $"Details:"
+                Text = Loc.GetString("salvage-expedition-window-details")
             });
 
             lBox.AddChild(new Label
             {
-                Text = missionDetails,
+                Text = details,
                 FontColorOverride = StyleNano.NanoGold,
                 HorizontalAlignment = HAlignment.Left,
                 Margin = new Thickness(0f, 0f, 0f, 5f),
@@ -139,9 +126,11 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
             // Details
             lBox.AddChild(new Label
             {
-                Text = $"Hostiles:"
+                Text = Loc.GetString("salvage-expedition-window-hostiles")
             });
 
+            var faction = mission.Faction;
+
             lBox.AddChild(new Label
             {
                 Text = faction,
@@ -153,7 +142,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
             // Duration
             lBox.AddChild(new Label
             {
-                Text = $"Duration:"
+                Text = Loc.GetString("salvage-expedition-window-duration")
             });
 
             lBox.AddChild(new Label
@@ -167,72 +156,45 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
             // Biome
             lBox.AddChild(new Label
             {
-                Text = "Biome:"
+                Text = Loc.GetString("salvage-expedition-window-biome")
             });
 
-            lBox.AddChild(new Label
-            {
-                Text = _prototype.Index<BiomePrototype>(config.Biome).Description,
-                FontColorOverride = StyleNano.NanoGold,
-                HorizontalAlignment = HAlignment.Left,
-                Margin = new Thickness(0f, 0f, 0f, 5f),
-            });
-
-            // Environment
-            lBox.AddChild(new Label
-            {
-                Text = "Environment:"
-            });
+            var biome = mission.Biome;
 
             lBox.AddChild(new Label
             {
-                Text = config.Description,
+                Text = Loc.GetString(_prototype.Index<SalvageBiomeMod>(biome).ID),
                 FontColorOverride = StyleNano.NanoGold,
                 HorizontalAlignment = HAlignment.Left,
                 Margin = new Thickness(0f, 0f, 0f, 5f),
             });
 
             // Modifiers
-            // TODO
-
-            // Rewards
-            lBox.AddChild(new Label()
+            lBox.AddChild(new Label
             {
-                Text = $"Reward:"
+                Text = Loc.GetString("salvage-expedition-window-modifiers")
             });
 
-            var salvageReward = SharedSalvageSystem.GetReward(_prototype.Index<WeightedRandomPrototype>(config.Reward), mission.Seed, _prototype);
-            var difficulty = config.DifficultyRating;
-            var rewardDesc = string.Empty;
+            var mods = mission.Modifiers;
 
-            switch (salvageReward)
-            {
-                case BankReward bank:
-                    rewardDesc = $"Bank payment of {(int) (bank.Amount * SharedSalvageSystem.GetDifficultyModifier(difficulty))}";
-                    break;
-                default:
-                    throw new ArgumentOutOfRangeException();
-            }
-
-            lBox.AddChild(new Label()
+            lBox.AddChild(new Label
             {
-                Text = rewardDesc,
-                FontColorOverride = StyleNano.GoodGreenFore,
+                Text = string.Join("\n", mods.Select(o => "- " + o)).TrimEnd(),
+                FontColorOverride = StyleNano.NanoGold,
                 HorizontalAlignment = HAlignment.Left,
                 Margin = new Thickness(0f, 0f, 0f, 5f),
             });
 
-
             lBox.AddChild(new Label()
             {
-                Text = $"Materials:"
+                Text = Loc.GetString("salvage-expedition-window-loot")
             });
 
-            if (config.Loots.Count == 0)
+            if (mission.Loot.Count == 0)
             {
                 lBox.AddChild(new Label()
                 {
-                    Text = "N/A",
+                    Text = Loc.GetString("salvage-expedition-window-none"),
                     FontColorOverride = StyleNano.ConcerningOrangeFore,
                     HorizontalAlignment = HAlignment.Left,
                     Margin = new Thickness(0f, 0f, 0f, 5f),
@@ -240,60 +202,57 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
             }
             else
             {
-                foreach (var lootProto in SharedSalvageSystem.GetLoot(config.Loots, mission.Seed, _prototype))
+                lBox.AddChild(new Label()
                 {
-                    lBox.AddChild(new Label()
-                    {
-                        Text = lootProto.Description,
-                        FontColorOverride = StyleNano.ConcerningOrangeFore,
-                        HorizontalAlignment = HAlignment.Left,
-                        Margin = new Thickness(0f, 0f, 0f, 5f),
-                    });
-                }
+                    Text = string.Join("\n", mission.Loot.Select(o => "- " + _prototype.Index<SalvageLootPrototype>(o.Key).Description + (o.Value > 1 ? $" x {o.Value}" : ""))).TrimEnd(),
+                    FontColorOverride = StyleNano.ConcerningOrangeFore,
+                    HorizontalAlignment = HAlignment.Left,
+                    Margin = new Thickness(0f, 0f, 0f, 5f),
+                });
             }
 
             // Claim
             var claimButton = new Button()
             {
                 HorizontalExpand = true,
-                Pressed = state.ActiveMission == mission.Index,
+                VerticalAlignment = VAlignment.Bottom,
+                Pressed = state.ActiveMission == missionParams.Index,
                 ToggleMode = true,
-                Disabled = state.Claimed,
+                Disabled = state.Claimed || state.Cooldown,
             };
 
             claimButton.Label.Margin = new Thickness(0f, 5f);
 
             claimButton.OnPressed += args =>
             {
-                ClaimMission?.Invoke(mission.Index);
+                ClaimMission?.Invoke(missionParams.Index);
             };
 
-            if (state.ActiveMission == mission.Index)
+            if (state.ActiveMission == missionParams.Index)
             {
-                claimButton.Text = "Claimed";
+                claimButton.Text = Loc.GetString("salvage-expedition-window-claimed");
                 claimButton.AddStyleClass(StyleBase.ButtonCaution);
             }
             else
             {
-                claimButton.Text = "Claim";
+                claimButton.Text = Loc.GetString("salvage-expedition-window-claim");
             }
 
-            // TODO: Fix this copypaste bullshit
-
-            var box = new PanelContainer()
+            var box = new PanelContainer
             {
                 PanelOverride = new StyleBoxFlat(new Color(30, 30, 34)),
                 HorizontalExpand = true,
                 Margin = new Thickness(5f, 0f),
                 Children =
                 {
-                    new BoxContainer()
+                    new BoxContainer
                     {
                         Orientation = BoxContainer.LayoutOrientation.Vertical,
                         Children =
                         {
                             missionStripe,
                             lBox,
+                            new Control() {VerticalExpand = true},
                             claimButton,
                         },
                         Margin = new Thickness(5f, 5f)
@@ -314,7 +273,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
         if (_claimed)
         {
             NextOfferBar.Value = 0f;
-            NextOfferText.Text = "N/A";
+            NextOfferText.Text = "00:00";
             return;
         }
 
@@ -327,7 +286,11 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
         }
         else
         {
-            NextOfferBar.Value = 1f - (float) (remaining / SharedSalvageSystem.MissionCooldown);
+            var cooldown = _cooldown
+                ? SharedSalvageSystem.MissionFailedCooldown
+                : SharedSalvageSystem.MissionCooldown;
+
+            NextOfferBar.Value = 1f - (float) (remaining / cooldown);
             NextOfferText.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
         }
     }
index 642b5d88d44528f641507a12a8ed35c937f6354d..675fedc807b96fd6421d4282a67a8adf46e3b8fd 100644 (file)
@@ -23,7 +23,6 @@ using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Administration.Managers;
 using Content.Shared.Kitchen;
-using Content.Shared.Module;
 
 namespace Content.Server.IoC
 {
index 5f1a9a5c12acf557c3bd5132c37346010e2c6977..902d25388b1de8d0174f8c14c3a7536c9b034ffd 100644 (file)
@@ -53,7 +53,7 @@ public sealed class PlanetCommand : IConsoleCommand
             return;
         }
 
-        if (!_protoManager.HasIndex<BiomePrototype>(args[1]))
+        if (!_protoManager.TryIndex<BiomeTemplatePrototype>(args[1], out var biomeTemplate))
         {
             shell.WriteError(Loc.GetString("cmd-planet-map-prototype", ("prototype", args[1])));
             return;
@@ -63,8 +63,9 @@ public sealed class PlanetCommand : IConsoleCommand
         MetaDataComponent? metadata = null;
 
         var biome = _entManager.EnsureComponent<BiomeComponent>(mapUid);
-        _entManager.System<BiomeSystem>().SetPrototype(biome, args[1]);
-        _entManager.System<BiomeSystem>().SetSeed(biome, _random.Next());
+        var biomeSystem = _entManager.System<BiomeSystem>();
+        biomeSystem.SetSeed(biome, _random.Next());
+        biomeSystem.SetTemplate(biome, biomeTemplate);
         _entManager.Dirty(biome);
 
         var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
@@ -106,7 +107,7 @@ public sealed class PlanetCommand : IConsoleCommand
 
         if (args.Length == 2)
         {
-            var options = _protoManager.EnumeratePrototypes<BiomePrototype>()
+            var options = _protoManager.EnumeratePrototypes<BiomeTemplatePrototype>()
                 .Select(o => new CompletionOption(o.ID, "Biome"));
             return CompletionResult.FromOptions(options);
         }
index d07aecd1ea47d698f0abf71151fc82b68f44946d..ad4cbf27d30466823456e913d123a596ef659422 100644 (file)
@@ -162,8 +162,9 @@ public sealed class HTNSystem : EntitySystem
     public void UpdateNPC(ref int count, int maxUpdates, float frameTime)
     {
         _planQueue.Process();
+        var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
 
-        foreach (var (_, comp) in EntityQuery<ActiveNPCComponent, HTNComponent>())
+        while(query.MoveNext(out var uid, out _, out var comp))
         {
             // If we're over our max count or it's not MapInit then ignore the NPC.
             if (count >= maxUpdates)
@@ -173,10 +174,10 @@ public sealed class HTNSystem : EntitySystem
             {
                 if (comp.PlanningJob.Exception != null)
                 {
-                    _sawmill.Fatal($"Received exception on planning job for {comp.Owner}!");
-                    _npc.SleepNPC(comp.Owner);
+                    _sawmill.Fatal($"Received exception on planning job for {uid}!");
+                    _npc.SleepNPC(uid);
                     var exc = comp.PlanningJob.Exception;
-                    RemComp<HTNComponent>(comp.Owner);
+                    RemComp<HTNComponent>(uid);
                     throw exc;
                 }
 
@@ -231,7 +232,7 @@ public sealed class HTNSystem : EntitySystem
 
                         RaiseNetworkEvent(new HTNMessage()
                         {
-                            Uid = comp.Owner,
+                            Uid = uid,
                             Text = text.ToString(),
                         }, session.ConnectedClient);
                     }
diff --git a/Content.Server/Parallax/BiomeSystem.Commands.cs b/Content.Server/Parallax/BiomeSystem.Commands.cs
new file mode 100644 (file)
index 0000000..0a60a3f
--- /dev/null
@@ -0,0 +1,172 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
+using Robust.Shared.Console;
+using Robust.Shared.Map;
+
+namespace Content.Server.Parallax;
+
+public sealed partial class BiomeSystem
+{
+    private void InitializeCommands()
+    {
+        _console.RegisterCommand("biome_clear", Loc.GetString("cmd-biome_clear-desc"), Loc.GetString("cmd-biome_clear-help"), BiomeClearCallback, BiomeClearCallbackHelper);
+        _console.RegisterCommand("biome_addlayer", Loc.GetString("cmd-biome_addlayer-desc"), Loc.GetString("cmd-biome_addlayer-help"), AddLayerCallback, AddLayerCallbackHelp);
+        _console.RegisterCommand("biome_addmarkerlayer", Loc.GetString("cmd-biome_addmarkerlayer-desc"), Loc.GetString("cmd-biome_addmarkerlayer-desc"), AddMarkerLayerCallback, AddMarkerLayerCallbackHelper);
+    }
+
+    [AdminCommand(AdminFlags.Fun)]
+    private void BiomeClearCallback(IConsoleShell shell, string argstr, string[] args)
+    {
+        if (args.Length != 1)
+        {
+            return;
+        }
+
+        int.TryParse(args[0], out var mapInt);
+        var mapId = new MapId(mapInt);
+
+        if (_mapManager.MapExists(mapId) ||
+            !TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
+        {
+            return;
+        }
+
+        ClearTemplate(biome);
+    }
+
+    private CompletionResult BiomeClearCallbackHelper(IConsoleShell shell, string[] args)
+    {
+        if (args.Length == 1)
+        {
+            return CompletionResult.FromHintOptions(CompletionHelper.Components<BiomeComponent>(args[0], EntityManager), "Biome");
+        }
+
+        return CompletionResult.Empty;
+    }
+
+    [AdminCommand(AdminFlags.Fun)]
+    private void AddLayerCallback(IConsoleShell shell, string argstr, string[] args)
+    {
+        if (args.Length < 3 || args.Length > 4)
+        {
+            return;
+        }
+
+        if (!int.TryParse(args[0], out var mapInt))
+        {
+            return;
+        }
+
+        var mapId = new MapId(mapInt);
+
+        if (!_mapManager.MapExists(mapId) || !TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
+        {
+            return;
+        }
+
+        if (!_proto.TryIndex<BiomeTemplatePrototype>(args[1], out var template))
+        {
+            return;
+        }
+
+        var offset = 0;
+
+        if (args.Length == 4)
+        {
+            int.TryParse(args[3], out offset);
+        }
+
+        AddTemplate(biome, args[2], template, offset);
+    }
+
+    private CompletionResult AddLayerCallbackHelp(IConsoleShell shell, string[] args)
+    {
+        if (args.Length == 1)
+        {
+            return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map ID");
+        }
+
+        if (args.Length == 2)
+        {
+            return CompletionResult.FromHintOptions(
+                CompletionHelper.PrototypeIDs<BiomeTemplatePrototype>(proto: _proto), "Biome template");
+        }
+
+        if (args.Length == 3)
+        {
+            if (int.TryParse(args[0], out var mapInt))
+            {
+                var mapId = new MapId(mapInt);
+
+                if (TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
+                {
+                    var results = new List<string>();
+
+                    foreach (var layer in biome.Layers)
+                    {
+                        if (layer is not BiomeDummyLayer dummy)
+                            continue;
+
+                        results.Add(dummy.ID);
+                    }
+
+                    return CompletionResult.FromHintOptions(results, "Dummy layer ID");
+                }
+            }
+        }
+
+        if (args.Length == 4)
+        {
+            return CompletionResult.FromHint("Seed offset");
+        }
+
+        return CompletionResult.Empty;
+    }
+
+    [AdminCommand(AdminFlags.Fun)]
+    private void AddMarkerLayerCallback(IConsoleShell shell, string argstr, string[] args)
+    {
+        if (args.Length != 2)
+        {
+            return;
+        }
+
+        if (!int.TryParse(args[0], out var mapInt))
+        {
+            return;
+        }
+
+        var mapId = new MapId(mapInt);
+
+        if (!_mapManager.MapExists(mapId) || !TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
+        {
+            return;
+        }
+
+        if (!_proto.HasIndex<BiomeMarkerLayerPrototype>(args[1]))
+        {
+            return;
+        }
+
+        biome.MarkerLayers.Add(args[1]);
+    }
+
+    private CompletionResult AddMarkerLayerCallbackHelper(IConsoleShell shell, string[] args)
+    {
+        if (args.Length == 1)
+        {
+            return CompletionResult.FromHintOptions(CompletionHelper.Components<BiomeComponent>(args[0], EntityManager), "Biome");
+        }
+
+        if (args.Length == 2)
+        {
+            return CompletionResult.FromHintOptions(
+                CompletionHelper.PrototypeIDs<BiomeMarkerLayerPrototype>(proto: _proto), "Marker");
+        }
+
+        return CompletionResult.Empty;
+    }
+}
index 5733d7327744d10c5cbbdadec52592c79d1a223d..e7227c0045c0576a7add90cb6afc502b0bbbe567 100644 (file)
@@ -1,21 +1,31 @@
 using Content.Server.Decals;
+using Content.Server.Shuttles.Events;
 using Content.Shared.Decals;
 using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Parallax.Biomes.Points;
 using Robust.Server.Player;
 using Robust.Shared;
 using Robust.Shared.Configuration;
+using Robust.Shared.Console;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Noise;
 using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
+using Robust.Shared.Utility;
 
 namespace Content.Server.Parallax;
 
-public sealed class BiomeSystem : SharedBiomeSystem
+public sealed partial class BiomeSystem : SharedBiomeSystem
 {
     [Dependency] private readonly IConfigurationManager _configManager = default!;
+    [Dependency] private readonly IConsoleHost _console = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
     [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly DecalSystem _decals = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
@@ -23,6 +33,10 @@ public sealed class BiomeSystem : SharedBiomeSystem
     private readonly HashSet<EntityUid> _handledEntities = new();
     private const float DefaultLoadRange = 16f;
     private float _loadRange = DefaultLoadRange;
+
+    /// <summary>
+    /// Load area for chunks containing tiles, decals etc.
+    /// </summary>
     private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange);
 
     /// <summary>
@@ -30,18 +44,41 @@ public sealed class BiomeSystem : SharedBiomeSystem
     /// </summary>
     private readonly Dictionary<BiomeComponent, HashSet<Vector2i>> _activeChunks = new();
 
+    private readonly Dictionary<BiomeComponent,
+        Dictionary<string, HashSet<Vector2i>>> _markerChunks = new();
+
     public override void Initialize()
     {
         base.Initialize();
         SubscribeLocalEvent<BiomeComponent, ComponentStartup>(OnBiomeStartup);
         SubscribeLocalEvent<BiomeComponent, MapInitEvent>(OnBiomeMapInit);
+        SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
         _configManager.OnValueChanged(CVars.NetMaxUpdateRange, SetLoadRange, true);
+        InitializeCommands();
+        _proto.PrototypesReloaded += ProtoReload;
     }
 
     public override void Shutdown()
     {
         base.Shutdown();
         _configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, SetLoadRange);
+        _proto.PrototypesReloaded -= ProtoReload;
+    }
+
+    private void ProtoReload(PrototypesReloadedEventArgs obj)
+    {
+        if (!obj.ByType.TryGetValue(typeof(BiomeTemplatePrototype), out var reloads))
+            return;
+
+        var query = AllEntityQuery<BiomeComponent>();
+
+        while (query.MoveNext(out var biome))
+        {
+            if (biome.Template == null || !reloads.Modified.TryGetValue(biome.Template, out var proto))
+                continue;
+
+            SetTemplate(biome, (BiomeTemplatePrototype) proto);
+        }
     }
 
     private void SetLoadRange(float obj)
@@ -61,22 +98,127 @@ public sealed class BiomeSystem : SharedBiomeSystem
         SetSeed(component, _random.Next());
     }
 
-    public void SetPrototype(BiomeComponent component, string proto)
+    public void SetSeed(BiomeComponent component, int seed)
+    {
+        component.Seed = seed;
+        component.Noise.SetSeed(seed);
+        Dirty(component);
+    }
+
+    public void ClearTemplate(BiomeComponent component)
     {
-        if (component.BiomePrototype == proto)
+        component.Layers.Clear();
+        component.Template = null;
+        Dirty(component);
+    }
+
+    /// <summary>
+    /// Sets the <see cref="BiomeComponent.Template"/> and refreshes layers.
+    /// </summary>
+    public void SetTemplate(BiomeComponent component, BiomeTemplatePrototype template)
+    {
+        component.Layers.Clear();
+        component.Template = template.ID;
+
+        foreach (var layer in template.Layers)
+        {
+            component.Layers.Add(layer);
+        }
+
+        Dirty(component);
+    }
+
+    /// <summary>
+    /// Adds the specified layer at the specified marker if it exists.
+    /// </summary>
+    public void AddLayer(BiomeComponent component, string id, IBiomeLayer addedLayer, int seedOffset = 0)
+    {
+        for (var i = 0; i < component.Layers.Count; i++)
+        {
+            var layer = component.Layers[i];
+
+            if (layer is not BiomeDummyLayer dummy || dummy.ID != id)
+                continue;
+
+            addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset);
+            component.Layers.Insert(i, addedLayer);
+            break;
+        }
+
+        Dirty(component);
+    }
+
+    public void AddMarkerLayer(BiomeComponent component, string marker)
+    {
+        if (!_proto.HasIndex<BiomeMarkerLayerPrototype>(marker))
+        {
+            // TODO: Log when we get a sawmill
             return;
+        }
 
-        component.BiomePrototype = proto;
+        component.MarkerLayers.Add(marker);
         Dirty(component);
     }
 
-    public void SetSeed(BiomeComponent component, int seed)
+    /// <summary>
+    /// Adds the specified template at the specified marker if it exists, withour overriding every layer.
+    /// </summary>
+    public void AddTemplate(BiomeComponent component, string id, BiomeTemplatePrototype template, int seedOffset = 0)
     {
-        component.Seed = seed;
-        component.Noise.SetSeed(seed);
+        for (var i = 0; i < component.Layers.Count; i++)
+        {
+            var layer = component.Layers[i];
+
+            if (layer is not BiomeDummyLayer dummy || dummy.ID != id)
+                continue;
+
+            for (var j = template.Layers.Count - 1; j >= 0; j--)
+            {
+                var addedLayer = template.Layers[j];
+                addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset);
+                component.Layers.Insert(i, addedLayer);
+            }
+
+            break;
+        }
+
         Dirty(component);
     }
 
+    private void OnFTLStarted(ref FTLStartedEvent ev)
+    {
+        var targetMap = ev.TargetCoordinates.ToMap(EntityManager, _transform);
+        var targetMapUid = _mapManager.GetMapEntityId(targetMap.MapId);
+
+        if (!TryComp<BiomeComponent>(targetMapUid, out var biome))
+            return;
+
+        var targetArea = new Box2(targetMap.Position - 64f, targetMap.Position + 64f);
+        Preload(targetMapUid, biome, targetArea);
+    }
+
+    /// <summary>
+    /// Preloads biome for the specified area.
+    /// </summary>
+    public void Preload(EntityUid uid, BiomeComponent component, Box2 area)
+    {
+        var markers = component.MarkerLayers;
+        var goobers = _markerChunks.GetOrNew(component);
+
+        foreach (var layer in markers)
+        {
+            var proto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+            var enumerator = new ChunkIndicesEnumerator(area, proto.Size);
+
+            while (enumerator.MoveNext(out var chunk))
+            {
+                var chunkOrigin = chunk * proto.Size;
+                var layerChunks = goobers.GetOrNew(proto.ID);
+                layerChunks.Add(chunkOrigin.Value);
+            }
+        }
+    }
+
     public override void Update(float frameTime)
     {
         base.Update(frameTime);
@@ -87,6 +229,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
         while (biomes.MoveNext(out var biome))
         {
             _activeChunks.Add(biome, new HashSet<Vector2i>());
+            _markerChunks.GetOrNew(biome);
         }
 
         // Get chunks in range
@@ -98,7 +241,14 @@ public sealed class BiomeSystem : SharedBiomeSystem
                 _handledEntities.Add(pSession.AttachedEntity.Value) &&
                  biomeQuery.TryGetComponent(xform.MapUid, out var biome))
             {
-                AddChunksInRange(biome, _transform.GetWorldPosition(xform, xformQuery));
+                var worldPos = _transform.GetWorldPosition(xform, xformQuery);
+                AddChunksInRange(biome, worldPos);
+
+                foreach (var layer in biome.MarkerLayers)
+                {
+                    var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+                    AddMarkerChunksInRange(biome, worldPos, layerProto);
+                }
             }
 
             foreach (var viewer in pSession.ViewSubscriptions)
@@ -110,16 +260,22 @@ public sealed class BiomeSystem : SharedBiomeSystem
                     continue;
                 }
 
-                AddChunksInRange(biome, _transform.GetWorldPosition(xform, xformQuery));
+                var worldPos = _transform.GetWorldPosition(xform, xformQuery);
+                AddChunksInRange(biome, worldPos);
+
+                foreach (var layer in biome.MarkerLayers)
+                {
+                    var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+                    AddMarkerChunksInRange(biome, worldPos, layerProto);
+                }
             }
         }
 
         var loadBiomes = AllEntityQuery<BiomeComponent, MapGridComponent>();
 
-        while (loadBiomes.MoveNext(out var biome, out var grid))
+        while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
         {
             var noise = biome.Noise;
-            var gridUid = grid.Owner;
 
             // Load new chunks
             LoadChunks(biome, gridUid, grid, noise, xformQuery);
@@ -129,6 +285,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
 
         _handledEntities.Clear();
         _activeChunks.Clear();
+        _markerChunks.Clear();
     }
 
     private void AddChunksInRange(BiomeComponent biome, Vector2 worldPos)
@@ -141,6 +298,21 @@ public sealed class BiomeSystem : SharedBiomeSystem
         }
     }
 
+    private void AddMarkerChunksInRange(BiomeComponent biome, Vector2 worldPos, IBiomeMarkerLayer layer)
+    {
+        // Offset the load area so it's centralised.
+        var loadArea = new Box2(0, 0, layer.Size, layer.Size);
+        var enumerator = new ChunkIndicesEnumerator(loadArea.Translated(worldPos - layer.Size / 2f), layer.Size);
+
+        while (enumerator.MoveNext(out var chunkOrigin))
+        {
+            var lay = _markerChunks[biome].GetOrNew(layer.ID);
+            lay.Add(chunkOrigin.Value * layer.Size);
+        }
+    }
+
+    #region Load
+
     private void LoadChunks(
         BiomeComponent component,
         EntityUid gridUid,
@@ -148,8 +320,54 @@ public sealed class BiomeSystem : SharedBiomeSystem
         FastNoiseLite noise,
         EntityQuery<TransformComponent> xformQuery)
     {
+        var markers = _markerChunks[component];
+        var loadedMarkers = component.LoadedMarkers;
+
+        foreach (var (layer, chunks) in markers)
+        {
+            foreach (var chunk in chunks)
+            {
+                if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
+                    continue;
+
+                var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+                var buffer = layerProto.Radius / 2f;
+                mobChunks ??= new HashSet<Vector2i>();
+                mobChunks.Add(chunk);
+                loadedMarkers[layer] = mobChunks;
+                var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y);
+
+                // Load NOW
+                // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth
+                var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius));
+
+                for (var i = 0; i < count; i++)
+                {
+                    for (var j = 0; j < 5; j++)
+                    {
+                        var point = new Vector2(
+                            chunk.X + buffer * rand.NextFloat() * (layerProto.Size - buffer),
+                            chunk.Y + buffer * rand.NextFloat() * (layerProto.Size - buffer));
+
+                        var coords = new EntityCoordinates(gridUid, point);
+                        var tile = grid.LocalToTile(coords);
+
+                        // Blocked spawn, try again.
+                        if (grid.GetAnchoredEntitiesEnumerator(tile).MoveNext(out _))
+                            continue;
+
+                        for (var k = 0; k < layerProto.GroupCount; k++)
+                        {
+                            Spawn(layerProto.Prototype, new EntityCoordinates(gridUid, point));
+                        }
+
+                        break;
+                    }
+                }
+            }
+        }
+
         var active = _activeChunks[component];
-        var prototype = ProtoManager.Index<BiomePrototype>(component.BiomePrototype);
         List<(Vector2i, Tile)>? tiles = null;
 
         foreach (var chunk in active)
@@ -159,7 +377,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
 
             tiles ??= new List<(Vector2i, Tile)>(ChunkSize * ChunkSize);
             // Load NOW!
-            LoadChunk(component, gridUid, grid, chunk, noise, prototype, tiles, xformQuery);
+            LoadChunk(component, gridUid, grid, chunk, noise, tiles, xformQuery);
         }
     }
 
@@ -169,7 +387,6 @@ public sealed class BiomeSystem : SharedBiomeSystem
         MapGridComponent grid,
         Vector2i chunk,
         FastNoiseLite noise,
-        BiomePrototype prototype,
         List<(Vector2i, Tile)> tiles,
         EntityQuery<TransformComponent> xformQuery)
     {
@@ -191,7 +408,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
                     continue;
 
                 // Pass in null so we don't try to get the tileref.
-                if (!TryGetBiomeTile(indices, prototype, noise, null, out var biomeTile) || biomeTile.Value == tileRef.Tile)
+                if (!TryGetBiomeTile(indices, component.Layers, noise, null, out var biomeTile) || biomeTile.Value == tileRef.Tile)
                     continue;
 
                 tiles.Add((indices, biomeTile.Value));
@@ -217,7 +434,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
                 // Don't mess with anything that's potentially anchored.
                 var anchored = grid.GetAnchoredEntitiesEnumerator(indices);
 
-                if (anchored.MoveNext(out _) || !TryGetEntity(indices, prototype, noise, grid, out var entPrototype))
+                if (anchored.MoveNext(out _) || !TryGetEntity(indices, component.Layers, noise, grid, out var entPrototype))
                     continue;
 
                 // TODO: Fix non-anchored ents spawning.
@@ -227,7 +444,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
                 // At least for now unless we do lookups or smth, only work with anchoring.
                 if (xformQuery.TryGetComponent(ent, out var xform) && !xform.Anchored)
                 {
-                    _transform.AnchorEntity(ent, xform, grid, indices);
+                    _transform.AnchorEntity(ent, xform, gridUid, grid, indices);
                 }
 
                 loadedEntities.Add(ent);
@@ -250,7 +467,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
                 // Don't mess with anything that's potentially anchored.
                 var anchored = grid.GetAnchoredEntitiesEnumerator(indices);
 
-                if (anchored.MoveNext(out _) || !TryGetDecals(indices, prototype, noise, grid, out var decals))
+                if (anchored.MoveNext(out _) || !TryGetDecals(indices, component.Layers, noise, grid, out var decals))
                     continue;
 
                 foreach (var decal in decals)
@@ -273,6 +490,10 @@ public sealed class BiomeSystem : SharedBiomeSystem
         }
     }
 
+    #endregion
+
+    #region Unload
+
     private void UnloadChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, FastNoiseLite noise)
     {
         var active = _activeChunks[component];
@@ -292,7 +513,6 @@ public sealed class BiomeSystem : SharedBiomeSystem
     private void UnloadChunk(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i chunk, FastNoiseLite noise, List<(Vector2i, Tile)> tiles)
     {
         // Reverse order to loading
-        var prototype = ProtoManager.Index<BiomePrototype>(component.BiomePrototype);
         component.ModifiedTiles.TryGetValue(chunk, out var modified);
         modified ??= new HashSet<Vector2i>();
 
@@ -338,7 +558,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
                 }
 
                 // If it's default data unload the tile.
-                if (!TryGetBiomeTile(indices, prototype, noise, null, out var biomeTile) ||
+                if (!TryGetBiomeTile(indices, component.Layers, noise, null, out var biomeTile) ||
                     grid.TryGetTileRef(indices, out var tileRef) && tileRef.Tile != biomeTile.Value)
                 {
                     modified.Add(indices);
@@ -362,4 +582,6 @@ public sealed class BiomeSystem : SharedBiomeSystem
             component.ModifiedTiles[chunk] = modified;
         }
     }
+
+    #endregion
 }
index f9b8f826f562e431e3c6901fa98752df3ab6393e..28f4eb5e4e1bd127834fefa8e339498b184819c5 100644 (file)
@@ -125,7 +125,10 @@ public sealed partial class DungeonJob
         }
 
         var tiles = new List<(Vector2i, Tile)>();
-        var dungeon = new Dungeon();
+        var dungeon = new Dungeon()
+        {
+            Position = _position
+        };
         var availablePacks = new List<DungeonRoomPackPrototype>();
         var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count];
         var packTransforms = new Matrix3[gen.RoomPacks.Count];
@@ -424,6 +427,16 @@ public sealed partial class DungeonJob
             }
         }
 
+        // Calculate center
+        var dungeonCenter = Vector2.Zero;
+
+        foreach (var room in dungeon.Rooms)
+        {
+            dungeonCenter += room.Center;
+        }
+
+        dungeon.Center = (Vector2i) (dungeonCenter / dungeon.Rooms.Count);
+
         return dungeon;
     }
 }
index 9f1e410a6054223b7def0b633545c453e01e4488..a5d4d0101fd4b9371311366e80684a1b0a480d9b 100644 (file)
@@ -110,13 +110,11 @@ public sealed partial class DungeonJob
         var rooms = new List<DungeonRoom>(dungeon.Rooms);
         var roomTiles = new List<Vector2i>();
         var tileData = new Tile(_tileDefManager[gen.Tile].TileId);
-        var count = gen.Count;
 
-        while (count > 0 && rooms.Count > 0)
+        for (var i = 0; i < gen.Count; i++)
         {
             var roomIndex = random.Next(rooms.Count);
             var room = rooms[roomIndex];
-            rooms.RemoveAt(roomIndex);
 
             // Move out 3 tiles in a direction away from center of the room
             // If none of those intersect another tile it's probably external
@@ -126,12 +124,6 @@ public sealed partial class DungeonJob
 
             foreach (var tile in roomTiles)
             {
-                // Check the interior node is at least accessible?
-                // Can't do anchored because it might be a locker or something.
-                // TODO: Better collision mask check
-                if (_lookup.GetEntitiesIntersecting(gridUid, tile, LookupFlags.Dynamic | LookupFlags.Static).Any())
-                    continue;
-
                 var direction = (tile - room.Center).ToAngle().GetCardinalDir().ToAngle().ToVec();
                 var isValid = true;
 
@@ -163,8 +155,6 @@ public sealed partial class DungeonJob
                     _entManager.SpawnEntity(ent, gridCoords);
                 }
 
-                count--;
-
                 // Clear out any biome tiles nearby to avoid blocking it
                 foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false))
                 {
index e2947570369246e6efffe28c567e1d39832fa8e7..29abd2dc124e385b159fbe95a5dfec9955a5ad12 100644 (file)
@@ -28,7 +28,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
 
     private readonly DungeonConfigPrototype _gen;
     private readonly int _seed;
-    private readonly Vector2 _position;
+    private readonly Vector2i _position;
 
     private readonly MapGridComponent _grid;
     private readonly EntityUid _gridUid;
@@ -51,7 +51,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
         MapGridComponent grid,
         EntityUid gridUid,
         int seed,
-        Vector2 position,
+        Vector2i position,
         CancellationToken cancellation = default) : base(maxTime, cancellation)
     {
         _sawmill = sawmill;
index 3b0058ca9af4a48c649b6780cae46fecf93bb926..d783eb60c63e85c4120bfca2d83defdf1be7d768 100644 (file)
@@ -43,7 +43,7 @@ public sealed partial class DungeonSystem
             return;
         }
 
-        var position = new Vector2(posX, posY);
+        var position = new Vector2i(posX, posY);
         var dungeonUid = _mapManager.GetMapEntityId(mapId);
 
         if (!TryComp<MapGridComponent>(dungeonUid, out var dungeonGrid))
index f8976d0043039b9c01ddb8f4bae59ce46988a176..083c635b7879f9d1e0c515653c64d0bee7af54e4 100644 (file)
@@ -161,7 +161,7 @@ public sealed partial class DungeonSystem : EntitySystem
     public void GenerateDungeon(DungeonConfigPrototype gen,
         EntityUid gridUid,
         MapGridComponent grid,
-        Vector2 position,
+        Vector2i position,
         int seed)
     {
         var cancelToken = new CancellationTokenSource();
@@ -193,7 +193,7 @@ public sealed partial class DungeonSystem : EntitySystem
         DungeonConfigPrototype gen,
         EntityUid gridUid,
         MapGridComponent grid,
-        Vector2 position,
+        Vector2i position,
         int seed)
     {
         var cancelToken = new CancellationTokenSource();
diff --git a/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs
new file mode 100644 (file)
index 0000000..452ac9d
--- /dev/null
@@ -0,0 +1,44 @@
+using Content.Shared.Salvage;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.Salvage.Expeditions;
+
+/// <summary>
+/// Designates this entity as holding a salvage expedition.
+/// </summary>
+[RegisterComponent]
+public sealed class SalvageExpeditionComponent : Component
+{
+    public SalvageMissionParams MissionParams = default!;
+
+    /// <summary>
+    /// Where the dungeon is located for initial announcement.
+    /// </summary>
+    [DataField("dungeonLocation")]
+    public Vector2 DungeonLocation = Vector2.Zero;
+
+    /// <summary>
+    /// When the expeditions ends.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan EndTime;
+
+    /// <summary>
+    /// Station whose mission this is.
+    /// </summary>
+    [ViewVariables, DataField("station")]
+    public EntityUid Station;
+
+    [ViewVariables] public bool Completed = false;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField("stage")]
+    public ExpeditionStage Stage = ExpeditionStage.Added;
+}
+
+public enum ExpeditionStage : byte
+{
+    Added,
+    Running,
+    Countdown,
+    FinalCountdown,
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs
new file mode 100644 (file)
index 0000000..6aee2cc
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Server.Salvage.Expeditions;
+
+/// <summary>
+/// Tracks expedition data for <see cref="SalvageMissionType.Mining"/>
+/// </summary>
+[RegisterComponent, Access(typeof(SalvageSystem))]
+public sealed class SalvageMiningExpeditionComponent : Component
+{
+    /// <summary>
+    /// Entities that were present on the shuttle and match the loot tax.
+    /// </summary>
+    [DataField("exemptEntities")]
+    public List<EntityUid> ExemptEntities = new();
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs b/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs
new file mode 100644 (file)
index 0000000..3abce55
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Server.Salvage.Expeditions.Structure;
+
+/// <summary>
+/// Mission objective for salvage expeditions.
+/// </summary>
+[RegisterComponent, Access(typeof(SalvageSystem))]
+public sealed class SalvageStructureComponent : Component
+{
+
+}
diff --git a/Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs b/Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs
new file mode 100644 (file)
index 0000000..daa704c
--- /dev/null
@@ -0,0 +1,13 @@
+using Content.Shared.Salvage;
+
+namespace Content.Server.Salvage.Expeditions.Structure;
+
+/// <summary>
+/// Tracks expedition data for <see cref="SalvageMissionType.Structure"/>
+/// </summary>
+[RegisterComponent, Access(typeof(SalvageSystem), typeof(SpawnSalvageMissionJob))]
+public sealed class SalvageStructureExpeditionComponent : Component
+{
+    [DataField("structures")]
+    public readonly List<EntityUid> Structures = new();
+}
diff --git a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs
new file mode 100644 (file)
index 0000000..3239bc7
--- /dev/null
@@ -0,0 +1,67 @@
+using Content.Shared.Salvage;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Salvage;
+
+public sealed partial class SalvageSystem
+{
+    private void OnSalvageClaimMessage(EntityUid uid, SalvageExpeditionConsoleComponent component, ClaimSalvageMessage args)
+    {
+        var station = _station.GetOwningStation(uid);
+
+        if (!TryComp<SalvageExpeditionDataComponent>(station, out var data) || data.Claimed)
+            return;
+
+        if (!data.Missions.TryGetValue(args.Index, out var missionparams))
+            return;
+
+        SpawnMission(missionparams, station.Value);
+
+        data.ActiveMission = args.Index;
+        var mission = GetMission(missionparams.MissionType, missionparams.Difficulty, missionparams.Seed);
+        data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
+        UpdateConsoles(data);
+    }
+
+    private void OnSalvageConsoleInit(EntityUid uid, SalvageExpeditionConsoleComponent component, ComponentInit args)
+    {
+        UpdateConsole(component);
+    }
+
+    private void OnSalvageConsoleParent(EntityUid uid, SalvageExpeditionConsoleComponent component, ref EntParentChangedMessage args)
+    {
+        UpdateConsole(component);
+    }
+
+    private void UpdateConsoles(SalvageExpeditionDataComponent component)
+    {
+        var state = GetState(component);
+
+        foreach (var (console, xform, uiComp) in EntityQuery<SalvageExpeditionConsoleComponent, TransformComponent, ServerUserInterfaceComponent>(true))
+        {
+            var station = _station.GetOwningStation(console.Owner, xform);
+
+            if (station != component.Owner)
+                continue;
+
+            _ui.TrySetUiState(console.Owner, SalvageConsoleUiKey.Expedition, state, ui: uiComp);
+        }
+    }
+
+    private void UpdateConsole(SalvageExpeditionConsoleComponent component)
+    {
+        var station = _station.GetOwningStation(component.Owner);
+        SalvageExpeditionConsoleState state;
+
+        if (TryComp<SalvageExpeditionDataComponent>(station, out var dataComponent))
+        {
+            state = GetState(dataComponent);
+        }
+        else
+        {
+            state = new SalvageExpeditionConsoleState(TimeSpan.Zero, false, true, 0, new List<SalvageMissionParams>());
+        }
+
+        _ui.TrySetUiState(component.Owner, SalvageConsoleUiKey.Expedition, state);
+    }
+}
diff --git a/Content.Server/Salvage/SalvageSystem.Expeditions.cs b/Content.Server/Salvage/SalvageSystem.Expeditions.cs
new file mode 100644 (file)
index 0000000..9787e3a
--- /dev/null
@@ -0,0 +1,241 @@
+using System.Linq;
+using System.Threading;
+using Content.Server.CPUJob.JobQueues;
+using Content.Server.CPUJob.JobQueues.Queues;
+using Content.Server.Salvage.Expeditions;
+using Content.Server.Salvage.Expeditions.Structure;
+using Content.Server.Station.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Salvage;
+
+namespace Content.Server.Salvage;
+
+public sealed partial class SalvageSystem
+{
+    /*
+     * Handles setup / teardown of salvage expeditions.
+     */
+
+    private const int MissionLimit = 5;
+
+    private readonly JobQueue _salvageQueue = new();
+    private readonly List<(SpawnSalvageMissionJob Job, CancellationTokenSource CancelToken)> _salvageJobs = new();
+    private const double SalvageJobTime = 0.002;
+
+    private void InitializeExpeditions()
+    {
+        SubscribeLocalEvent<StationInitializedEvent>(OnSalvageExpStationInit);
+
+        SubscribeLocalEvent<SalvageExpeditionConsoleComponent, ComponentInit>(OnSalvageConsoleInit);
+        SubscribeLocalEvent<SalvageExpeditionConsoleComponent, EntParentChangedMessage>(OnSalvageConsoleParent);
+        SubscribeLocalEvent<SalvageExpeditionConsoleComponent, ClaimSalvageMessage>(OnSalvageClaimMessage);
+
+        SubscribeLocalEvent<SalvageExpeditionDataComponent, EntityUnpausedEvent>(OnDataUnpaused);
+
+        SubscribeLocalEvent<SalvageExpeditionComponent, ComponentShutdown>(OnExpeditionShutdown);
+        SubscribeLocalEvent<SalvageExpeditionComponent, EntityUnpausedEvent>(OnExpeditionUnpaused);
+
+        SubscribeLocalEvent<SalvageStructureComponent, ExaminedEvent>(OnStructureExamine);
+    }
+
+    private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent component, ComponentShutdown args)
+    {
+        foreach (var (job, cancelToken) in _salvageJobs.ToArray())
+        {
+            if (job.Station == component.Station)
+            {
+                cancelToken.Cancel();
+                _salvageJobs.Remove((job, cancelToken));
+            }
+        }
+
+        if (Deleted(component.Station))
+            return;
+
+        // Finish mission
+        if (TryComp<SalvageExpeditionDataComponent>(component.Station, out var data))
+        {
+            FinishExpedition(data, component, null);
+        }
+    }
+
+    private void OnDataUnpaused(EntityUid uid, SalvageExpeditionDataComponent component, ref EntityUnpausedEvent args)
+    {
+        component.NextOffer += args.PausedTime;
+    }
+
+    private void OnExpeditionUnpaused(EntityUid uid, SalvageExpeditionComponent component, ref EntityUnpausedEvent args)
+    {
+        component.EndTime += args.PausedTime;
+    }
+
+    private void OnSalvageExpStationInit(StationInitializedEvent ev)
+    {
+        EnsureComp<SalvageExpeditionDataComponent>(ev.Station);
+    }
+
+    private void UpdateExpeditions()
+    {
+        var currentTime = _timing.CurTime;
+        _salvageQueue.Process();
+
+        foreach (var (job, cancelToken) in _salvageJobs.ToArray())
+        {
+            switch (job.Status)
+            {
+                case JobStatus.Finished:
+                    _salvageJobs.Remove((job, cancelToken));
+                    break;
+            }
+        }
+
+        foreach (var comp in EntityQuery<SalvageExpeditionDataComponent>())
+        {
+            // Update offers
+            if (comp.NextOffer > currentTime || comp.Claimed)
+                continue;
+
+            comp.Cooldown = false;
+            comp.NextOffer += MissionCooldown;
+            GenerateMissions(comp);
+            UpdateConsoles(comp);
+        }
+
+        var query = EntityQueryEnumerator<SalvageExpeditionComponent>();
+
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (comp.EndTime < currentTime)
+            {
+                QueueDel(uid);
+            }
+        }
+    }
+
+    private void FinishExpedition(SalvageExpeditionDataComponent component, SalvageExpeditionComponent expedition, EntityUid? shuttle)
+    {
+        // Finish mission cleanup.
+        switch (expedition.MissionParams.MissionType)
+        {
+            // Handles the mining taxation.
+            case SalvageMissionType.Mining:
+                expedition.Completed = true;
+
+                if (shuttle != null && TryComp<SalvageMiningExpeditionComponent>(expedition.Owner, out var mining))
+                {
+                    var xformQuery = GetEntityQuery<TransformComponent>();
+                    var entities = new List<EntityUid>();
+                    MiningTax(entities, shuttle.Value, mining, xformQuery);
+
+                    var tax = GetMiningTax(expedition.MissionParams.Difficulty);
+                    _random.Shuffle(entities);
+
+                    // TODO: urgh this pr is already taking so long I'll do this later
+                    for (var i = 0; i < Math.Ceiling(entities.Count * tax); i++)
+                    {
+                        // QueueDel(entities[i]);
+                    }
+                }
+
+                break;
+        }
+
+        // Payout already handled elsewhere.
+        if (expedition.Completed)
+        {
+            _sawmill.Debug($"Completed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
+            component.NextOffer = _timing.CurTime + MissionCooldown;
+            Announce(expedition.Owner, Loc.GetString("salvage-expedition-mission-completed"));
+        }
+        else
+        {
+            _sawmill.Debug($"Failed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
+            component.NextOffer = _timing.CurTime + MissionFailedCooldown;
+            Announce(expedition.Owner, Loc.GetString("salvage-expedition-mission-failed"));
+        }
+
+        component.ActiveMission = 0;
+        component.Cooldown = true;
+        UpdateConsoles(component);
+    }
+
+    /// <summary>
+    /// Deducts ore tax for mining.
+    /// </summary>
+    private void MiningTax(List<EntityUid> entities, EntityUid entity, SalvageMiningExpeditionComponent mining, EntityQuery<TransformComponent> xformQuery)
+    {
+        if (!mining.ExemptEntities.Contains(entity))
+        {
+            entities.Add(entity);
+        }
+
+        var xform = xformQuery.GetComponent(entity);
+        var children = xform.ChildEnumerator;
+
+        while (children.MoveNext(out var child))
+        {
+            MiningTax(entities, child.Value, mining, xformQuery);
+        }
+    }
+
+    private void GenerateMissions(SalvageExpeditionDataComponent component)
+    {
+        component.Missions.Clear();
+        var configs = Enum.GetValues<SalvageMissionType>().ToList();
+
+        if (configs.Count == 0)
+            return;
+
+        for (var i = 0; i < MissionLimit; i++)
+        {
+            _random.Shuffle(configs);
+            var rating = (DifficultyRating) i;
+
+            foreach (var config in configs)
+            {
+                var mission = new SalvageMissionParams()
+                {
+                    Index = component.NextIndex,
+                    MissionType = config,
+                    Seed = _random.Next(),
+                    Difficulty = rating,
+                };
+
+                component.Missions[component.NextIndex++] = mission;
+                break;
+            }
+        }
+    }
+
+    private SalvageExpeditionConsoleState GetState(SalvageExpeditionDataComponent component)
+    {
+        var missions = component.Missions.Values.ToList();
+        return new SalvageExpeditionConsoleState(component.NextOffer, component.Claimed, component.Cooldown, component.ActiveMission, missions);
+    }
+
+    private void SpawnMission(SalvageMissionParams missionParams, EntityUid station)
+    {
+        var cancelToken = new CancellationTokenSource();
+        var job = new SpawnSalvageMissionJob(
+            SalvageJobTime,
+            EntityManager,
+            _timing,
+            _mapManager,
+            _prototypeManager,
+            _tileDefManager,
+            _biome,
+            _dungeon,
+            this,
+            station,
+            missionParams,
+            cancelToken.Token);
+
+        _salvageJobs.Add((job, cancelToken));
+        _salvageQueue.EnqueueJob(job);
+    }
+
+    private void OnStructureExamine(EntityUid uid, SalvageStructureComponent component, ExaminedEvent args)
+    {
+        args.PushMarkup(Loc.GetString("salvage-expedition-structure-examine"));
+    }
+}
diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs
new file mode 100644 (file)
index 0000000..9cb56ae
--- /dev/null
@@ -0,0 +1,194 @@
+using Content.Server.Salvage.Expeditions;
+using Content.Server.Salvage.Expeditions.Structure;
+using Content.Server.Shuttles.Components;
+using Content.Server.Shuttles.Events;
+using Content.Server.Shuttles.Systems;
+using Content.Server.Station.Components;
+using Content.Shared.Chat;
+using Content.Shared.Salvage;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Salvage;
+
+public sealed partial class SalvageSystem
+{
+    /*
+     * Handles actively running a salvage expedition.
+     */
+
+    private void InitializeRunner()
+    {
+        SubscribeLocalEvent<FTLRequestEvent>(OnFTLRequest);
+        SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
+        SubscribeLocalEvent<FTLCompletedEvent>(OnFTLCompleted);
+    }
+
+    /// <summary>
+    /// Announces status updates to salvage crewmembers on the state of the expedition.
+    /// </summary>
+    private void Announce(EntityUid mapUid, string text)
+    {
+        var mapId = Comp<MapComponent>(mapUid).MapId;
+
+        // I love TComms and chat!!!
+        _chat.ChatMessageToManyFiltered(
+            Filter.BroadcastMap(mapId),
+            ChatChannel.Radio,
+            text,
+            text,
+            _mapManager.GetMapEntityId(mapId),
+            false,
+            true,
+            null);
+    }
+
+    private void OnFTLRequest(ref FTLRequestEvent ev)
+    {
+        if (!HasComp<SalvageExpeditionComponent>(ev.MapUid) ||
+            !TryComp<FTLDestinationComponent>(ev.MapUid, out var dest))
+        {
+            return;
+        }
+
+        // Only one shuttle can occupy an expedition.
+        dest.Enabled = false;
+        _shuttleConsoles.RefreshShuttleConsoles();
+    }
+
+    private void OnFTLCompleted(ref FTLCompletedEvent args)
+    {
+        if (!TryComp<SalvageExpeditionComponent>(args.MapUid, out var component))
+            return;
+
+        // Someone FTLd there so start announcement
+        if (component.Stage != ExpeditionStage.Added)
+            return;
+
+        Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", (component.EndTime - _timing.CurTime).Minutes)));
+
+        if (component.DungeonLocation != Vector2.Zero)
+            Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-dungeon", ("direction", component.DungeonLocation.GetDir())));
+
+        component.Stage = ExpeditionStage.Running;
+    }
+
+    private void OnFTLStarted(ref FTLStartedEvent ev)
+    {
+        // Started a mining mission so work out exempt entities
+        if (TryComp<SalvageMiningExpeditionComponent>(
+                _mapManager.GetMapEntityId(ev.TargetCoordinates.ToMap(EntityManager, _transform).MapId),
+                out var mining))
+        {
+            var ents = new List<EntityUid>();
+            var xformQuery = GetEntityQuery<TransformComponent>();
+            MiningTax(ents, ev.Entity, mining, xformQuery);
+            mining.ExemptEntities = ents;
+        }
+
+        if (!TryComp<SalvageExpeditionComponent>(ev.FromMapUid, out var expedition) ||
+            !TryComp<SalvageExpeditionDataComponent>(expedition.Station, out var station))
+        {
+            return;
+        }
+
+        // Check if any shuttles remain.
+        var query = EntityQueryEnumerator<ShuttleComponent, TransformComponent>();
+
+        while (query.MoveNext(out _, out var xform))
+        {
+            if (xform.MapUid == ev.FromMapUid)
+                return;
+        }
+
+        // Last shuttle has left so finish the mission.
+        QueueDel(ev.FromMapUid.Value);
+    }
+
+    // Runs the expedition
+    private void UpdateRunner()
+    {
+        // Generic missions
+        var query = EntityQueryEnumerator<SalvageExpeditionComponent>();
+
+        // Run the basic mission timers (e.g. announcements, auto-FTL, completion, etc)
+        while (query.MoveNext(out var uid, out var comp))
+        {
+            if (comp.Completed)
+                continue;
+
+            var remaining = comp.EndTime - _timing.CurTime;
+
+            if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(30))
+            {
+                comp.Stage = ExpeditionStage.FinalCountdown;
+                Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-seconds", ("duration", TimeSpan.FromSeconds(30).Seconds)));
+            }
+            // TODO: Play song.
+            else if (comp.Stage < ExpeditionStage.Countdown && remaining < TimeSpan.FromMinutes(2))
+            {
+                comp.Stage = ExpeditionStage.Countdown;
+                Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", TimeSpan.FromMinutes(2).Minutes)));
+            }
+            // Auto-FTL out any shuttles
+            else if (remaining < TimeSpan.FromSeconds(ShuttleSystem.DefaultStartupTime) + TimeSpan.FromSeconds(0.5))
+            {
+                var ftlTime = (float) remaining.TotalSeconds;
+
+                if (remaining < TimeSpan.FromSeconds(ShuttleSystem.DefaultStartupTime))
+                {
+                    ftlTime = MathF.Max(0, (float) remaining.TotalSeconds - 0.5f);
+                }
+
+                ftlTime = MathF.Min(ftlTime, ShuttleSystem.DefaultStartupTime);
+                var shuttleQuery = AllEntityQuery<ShuttleComponent, TransformComponent>();
+
+                if (TryComp<StationDataComponent>(comp.Station, out var data))
+                {
+                    foreach (var member in data.Grids)
+                    {
+                        while (shuttleQuery.MoveNext(out var shuttleUid, out var shuttle, out var shuttleXform))
+                        {
+                            if (shuttleXform.MapUid != uid || HasComp<FTLComponent>(shuttleUid))
+                                continue;
+
+                            _shuttle.FTLTravel(shuttleUid, shuttle, member, ftlTime);
+                        }
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        // Mining missions: NOOP
+
+        // Structure missions
+        var structureQuery = EntityQueryEnumerator<SalvageStructureExpeditionComponent, SalvageExpeditionComponent>();
+
+        while (structureQuery.MoveNext(out var uid, out var structure, out var comp))
+        {
+            if (comp.Completed)
+                continue;
+
+            var structureAnnounce = false;
+
+            for (var i = 0; i < structure.Structures.Count; i++)
+            {
+                var objective = structure.Structures[i];
+
+                if (Deleted(objective))
+                {
+                    structure.Structures.RemoveSwap(i);
+                    structureAnnounce = true;
+                }
+            }
+
+            if (structureAnnounce)
+            {
+                Announce(uid, Loc.GetString("salvage-expedition-structures-remaining", ("count", structure.Structures.Count)));
+            }
+        }
+    }
+}
index 8fe166f3c743f193da1a6e9d30264818791212f6..3a3e2190b9d761421927c2ce9cc103a0bf0837f8 100644 (file)
@@ -16,10 +16,11 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 using System.Linq;
-using Content.Server.Cargo.Systems;
-using Content.Server.NPC.Pathfinding;
+using Content.Server.Chat.Managers;
+using Content.Server.Chat.Systems;
 using Content.Server.Parallax;
 using Content.Server.Procedural;
+using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Systems;
 using Robust.Shared.Timing;
 
@@ -27,19 +28,22 @@ namespace Content.Server.Salvage
 {
     public sealed partial class SalvageSystem : SharedSalvageSystem
     {
+        [Dependency] private readonly IChatManager _chat = default!;
         [Dependency] private readonly IConfigurationManager _configurationManager = default!;
         [Dependency] private readonly IGameTiming _timing = default!;
         [Dependency] private readonly IMapManager _mapManager = default!;
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
         [Dependency] private readonly IRobustRandom _random = default!;
+        [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
         [Dependency] private readonly BiomeSystem _biome = default!;
-        [Dependency] private readonly CargoSystem _cargo = default!;
         [Dependency] private readonly DungeonSystem _dungeon = default!;
         [Dependency] private readonly MapLoaderSystem _map = default!;
-        [Dependency] private readonly PathfindingSystem _pathfinding = default!;
         [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
         [Dependency] private readonly RadioSystem _radioSystem = default!;
         [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+        [Dependency] private readonly SharedTransformSystem _transform = default!;
+        [Dependency] private readonly ShuttleSystem _shuttle = default!;
+        [Dependency] private readonly ShuttleConsoleSystem _shuttleConsoles = default!;
         [Dependency] private readonly StationSystem _station = default!;
         [Dependency] private readonly UserInterfaceSystem _ui = default!;
 
@@ -62,6 +66,9 @@ namespace Content.Server.Salvage
 
             // Can't use RoundRestartCleanupEvent, I need to clean up before the grid, and components are gone to prevent the announcements
             SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
+
+            InitializeExpeditions();
+            InitializeRunner();
         }
 
         private void OnRoundEnd(GameRunLevelChangedEvent ev)
@@ -449,6 +456,9 @@ namespace Content.Server.Salvage
                     state.ActiveMagnets.Remove(magnet);
                 }
             }
+
+            UpdateExpeditions();
+            UpdateRunner();
         }
     }
 
diff --git a/Content.Server/Salvage/SpawnSalvageMissionJob.cs b/Content.Server/Salvage/SpawnSalvageMissionJob.cs
new file mode 100644 (file)
index 0000000..b7de8fa
--- /dev/null
@@ -0,0 +1,372 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.CPUJob.JobQueues;
+using Content.Server.Parallax;
+using Content.Server.Procedural;
+using Content.Server.Salvage.Expeditions;
+using Content.Server.Salvage.Expeditions.Structure;
+using Content.Shared.Atmos;
+using Content.Shared.Dataset;
+using Content.Shared.Gravity;
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Procedural;
+using Content.Shared.Procedural.Loot;
+using Content.Shared.Random;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Salvage;
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
+using Content.Shared.Storage;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Salvage;
+
+public sealed class SpawnSalvageMissionJob : Job<bool>
+{
+    private readonly IEntityManager _entManager;
+    private readonly IGameTiming _timing;
+    private readonly IMapManager _mapManager;
+    private readonly IPrototypeManager _prototypeManager;
+    private readonly ITileDefinitionManager _tileDefManager;
+    private readonly BiomeSystem _biome;
+    private readonly DungeonSystem _dungeon;
+    private readonly SalvageSystem _salvage;
+
+    public readonly EntityUid Station;
+    private readonly SalvageMissionParams _missionParams;
+
+    public SpawnSalvageMissionJob(
+        double maxTime,
+        IEntityManager entManager,
+        IGameTiming timing,
+        IMapManager mapManager,
+        IPrototypeManager protoManager,
+        ITileDefinitionManager tileDefManager,
+        BiomeSystem biome,
+        DungeonSystem dungeon,
+        SalvageSystem salvage,
+        EntityUid station,
+        SalvageMissionParams missionParams,
+        CancellationToken cancellation = default) : base(maxTime, cancellation)
+    {
+        _entManager = entManager;
+        _timing = timing;
+        _mapManager = mapManager;
+        _prototypeManager = protoManager;
+        _tileDefManager = tileDefManager;
+        _biome = biome;
+        _dungeon = dungeon;
+        _salvage = salvage;
+        Station = station;
+        _missionParams = missionParams;
+    }
+
+    protected override async Task<bool> Process()
+    {
+        Logger.DebugS("salvage", $"Spawning salvage mission with seed {_missionParams.Seed}");
+        var config = _missionParams.MissionType;
+        var mapId = _mapManager.CreateMap();
+        var mapUid = _mapManager.GetMapEntityId(mapId);
+        _mapManager.AddUninitializedMap(mapId);
+        MetaDataComponent? metadata = null;
+        var grid = _entManager.EnsureComponent<MapGridComponent>(mapUid);
+        var random = new Random(_missionParams.Seed);
+
+        // Setup mission configs
+        // As we go through the config the rating will deplete so we'll go for most important to least important.
+
+        var mission = _entManager.System<SharedSalvageSystem>()
+            .GetMission(_missionParams.MissionType, _missionParams.Difficulty, _missionParams.Seed);
+
+        var missionBiome = _prototypeManager.Index<SalvageBiomeMod>(mission.Biome);
+
+        if (missionBiome.BiomePrototype != null)
+        {
+            var biome = _entManager.AddComponent<BiomeComponent>(mapUid);
+            var biomeSystem = _entManager.System<BiomeSystem>();
+            biomeSystem.SetTemplate(biome, _prototypeManager.Index<BiomeTemplatePrototype>(missionBiome.BiomePrototype));
+            biomeSystem.SetSeed(biome, mission.Seed);
+            _entManager.Dirty(biome);
+
+            // Gravity
+            var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
+            gravity.Enabled = true;
+            _entManager.Dirty(gravity, metadata);
+
+            // Atmos
+            var atmos = _entManager.EnsureComponent<MapAtmosphereComponent>(mapUid);
+            atmos.Space = false;
+            var moles = new float[Atmospherics.AdjustedNumberOfGases];
+            moles[(int) Gas.Oxygen] = 21.824779f;
+            moles[(int) Gas.Nitrogen] = 82.10312f;
+
+            atmos.Mixture = new GasMixture(2500)
+            {
+                Temperature = 293.15f,
+                Moles = moles,
+            };
+
+            if (mission.Color != null)
+            {
+                var lighting = _entManager.EnsureComponent<MapLightComponent>(mapUid);
+                lighting.AmbientLightColor = mission.Color.Value;
+                _entManager.Dirty(lighting);
+            }
+        }
+
+        _mapManager.DoMapInitialize(mapId);
+        _mapManager.SetMapPaused(mapId, true);
+
+        // Setup expedition
+        var expedition = _entManager.AddComponent<SalvageExpeditionComponent>(mapUid);
+        expedition.Station = Station;
+        expedition.EndTime = _timing.CurTime + mission.Duration;
+        expedition.MissionParams = _missionParams;
+
+        // Don't want consoles to have the incorrect name until refreshed.
+        var ftlUid = _entManager.CreateEntityUninitialized("FTLPoint", new EntityCoordinates(mapUid, Vector2.Zero));
+        _entManager.GetComponent<MetaDataComponent>(ftlUid).EntityName = SharedSalvageSystem.GetFTLName(_prototypeManager.Index<DatasetPrototype>("names_borer"), _missionParams.Seed);
+        _entManager.InitializeAndStartEntity(ftlUid);
+
+        var landingPadRadius = 24;
+        var minDungeonOffset = landingPadRadius + 12;
+
+        var dungeonRotation = _dungeon.GetDungeonRotation(_missionParams.Seed);
+        var dungeonSpawnRotation = new Angle(random.NextDouble() * Math.Tau);
+
+        // If the dungeon were to spawn facing the landing pad then bump the offset a bit
+        // This isn't robust but fine for now.
+        if (Math.Abs((dungeonRotation - dungeonSpawnRotation).Theta) < Math.PI / 2)
+        {
+            minDungeonOffset += 16;
+        }
+
+        Dungeon dungeon = default!;
+
+        if (config != SalvageMissionType.Mining)
+        {
+            var maxDungeonOffset = minDungeonOffset + 24;
+            var dungeonOffsetDistance = minDungeonOffset + (maxDungeonOffset - minDungeonOffset) * random.NextFloat();
+            var dungeonOffset = new Vector2(dungeonOffsetDistance, 0f);
+            dungeonOffset = dungeonSpawnRotation.RotateVec(dungeonOffset);
+            var dungeonMod = _prototypeManager.Index<SalvageDungeonMod>(mission.Dungeon);
+            var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
+            dungeon =
+                await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
+                    _missionParams.Seed));
+
+            // Aborty
+            if (dungeon.Rooms.Count == 0)
+            {
+                return false;
+            }
+
+            expedition.DungeonLocation = dungeonOffset;
+        }
+
+        List<Vector2i> reservedTiles = new();
+
+        // Setup the landing pad
+        var landingPadExtents = new Vector2i(landingPadRadius, landingPadRadius);
+        var tiles = new List<(Vector2i Indices, Tile Tile)>(landingPadExtents.X * landingPadExtents.Y * 2);
+
+        // Set the tiles themselves
+        var landingTile = new Tile(_tileDefManager["FloorSteel"].TileId);
+
+        foreach (var tile in grid.GetTilesIntersecting(new Circle(Vector2.Zero, landingPadRadius), false))
+        {
+            if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _))
+                continue;
+
+            tiles.Add((tile.GridIndices, landingTile));
+            reservedTiles.Add(tile.GridIndices);
+        }
+
+        grid.SetTiles(tiles);
+
+        // Mission setup
+        switch (config)
+        {
+            case SalvageMissionType.Mining:
+                await SetupMining(mission, mapUid);
+                break;
+            case SalvageMissionType.Destruction:
+                await SetupStructure(mission, dungeon, mapUid, grid, random);
+                break;
+            default:
+                throw new NotImplementedException();
+        }
+
+        // Handle loot
+        foreach (var (loot, count) in mission.Loot)
+        {
+            for (var i = 0; i < count; i++)
+            {
+                var lootProto = _prototypeManager.Index<SalvageLootPrototype>(loot);
+                await SpawnDungeonLoot(dungeon, lootProto, mapUid, grid, random, reservedTiles);
+            }
+        }
+        return true;
+    }
+
+    private async Task SpawnDungeonLoot(Dungeon? dungeon, SalvageLootPrototype loot, EntityUid gridUid, MapGridComponent grid, Random random, List<Vector2i> reservedTiles)
+    {
+        for (var i = 0; i < loot.LootRules.Count; i++)
+        {
+            var rule = loot.LootRules[i];
+
+            switch (rule)
+            {
+                case BiomeTemplateLoot biomeLoot:
+                    if (_entManager.TryGetComponent<BiomeComponent>(gridUid, out var biome))
+                    {
+                        _biome.AddTemplate(biome, "Loot", _prototypeManager.Index<BiomeTemplatePrototype>(biomeLoot.Prototype), i);
+                    }
+                    break;
+                // Spawns a cluster (like an ore vein) nearby.
+                case DungeonClusterLoot clusterLoot:
+                    await SpawnDungeonClusterLoot(dungeon!, clusterLoot, grid, random, reservedTiles);
+                    break;
+            }
+        }
+    }
+
+    #region Loot
+
+    private async Task SpawnDungeonClusterLoot(
+        Dungeon dungeon,
+        DungeonClusterLoot loot,
+        MapGridComponent grid,
+        Random random,
+        List<Vector2i> reservedTiles)
+    {
+        var spawnTiles = new HashSet<Vector2i>();
+
+        for (var i = 0; i < loot.Points; i++)
+        {
+            var room = dungeon.Rooms[random.Next(dungeon.Rooms.Count)];
+            var clusterAmount = loot.ClusterAmount;
+            var spots = room.Tiles.ToList();
+            random.Shuffle(spots);
+
+            foreach (var spot in spots)
+            {
+                if (reservedTiles.Contains(spot))
+                    continue;
+
+                var anchored = grid.GetAnchoredEntitiesEnumerator(spot);
+
+                if (anchored.MoveNext(out _))
+                {
+                    continue;
+                }
+
+                clusterAmount--;
+                spawnTiles.Add(spot);
+
+                if (clusterAmount == 0)
+                    break;
+            }
+        }
+
+        foreach (var tile in spawnTiles)
+        {
+            await SuspendIfOutOfTime();
+            var proto = _prototypeManager.Index<WeightedRandomPrototype>(loot.Prototype).Pick(random);
+            _entManager.SpawnEntity(proto, grid.GridTileToLocal(tile));
+        }
+    }
+
+    #endregion
+
+    #region Mission Specific
+
+    private async Task SetupMining(
+        SalvageMission mission,
+        EntityUid gridUid)
+    {
+        var faction = _prototypeManager.Index<SalvageFactionPrototype>(mission.Faction);
+
+        if (_entManager.TryGetComponent<BiomeComponent>(gridUid, out var biome))
+        {
+            // TODO: Better
+            for (var i = 0; i < _salvage.GetDifficulty(mission.Difficulty); i++)
+            {
+                _biome.AddMarkerLayer(biome, faction.Configs["Mining"]);
+            }
+        }
+    }
+
+    private async Task SetupStructure(
+        SalvageMission mission,
+        Dungeon dungeon,
+        EntityUid gridUid,
+        MapGridComponent grid,
+        Random random)
+    {
+        var structureComp = _entManager.EnsureComponent<SalvageStructureExpeditionComponent>(gridUid);
+        var availableRooms = dungeon.Rooms.ToList();
+        var faction = _prototypeManager.Index<SalvageFactionPrototype>(mission.Faction);
+        await SpawnMobsRandomRooms(mission, dungeon, faction, grid, random);
+
+        var structureCount = _salvage.GetStructureCount(mission.Difficulty);
+        var shaggy = faction.Configs["DefenseStructure"];
+
+        // Spawn the objectives
+        for (var i = 0; i < structureCount; i++)
+        {
+            var structureRoom = availableRooms[random.Next(availableRooms.Count)];
+            var spawnTile = structureRoom.Tiles.ElementAt(random.Next(structureRoom.Tiles.Count));
+            var uid = _entManager.SpawnEntity(shaggy, grid.GridTileToLocal(spawnTile));
+            _entManager.AddComponent<SalvageStructureComponent>(uid);
+            structureComp.Structures.Add(uid);
+        }
+    }
+
+    private async Task SpawnMobsRandomRooms(SalvageMission mission, Dungeon dungeon, SalvageFactionPrototype faction, MapGridComponent grid, Random random)
+    {
+        var groupSpawns = _salvage.GetSpawnCount(mission.Difficulty);
+        var groupSum = faction.MobGroups.Sum(o => o.Prob);
+
+        for (var i = 0; i < groupSpawns; i++)
+        {
+            var roll = random.NextFloat() * groupSum;
+            var value = 0f;
+
+            foreach (var group in faction.MobGroups)
+            {
+                value += group.Prob;
+
+                if (value < roll)
+                    continue;
+
+                var mobGroupIndex = random.Next(faction.MobGroups.Count);
+                var mobGroup = faction.MobGroups[mobGroupIndex];
+
+                var spawnRoomIndex = random.Next(dungeon.Rooms.Count);
+                var spawnRoom = dungeon.Rooms[spawnRoomIndex];
+                var spawnTile = spawnRoom.Tiles.ElementAt(random.Next(spawnRoom.Tiles.Count));
+                var spawnPosition = grid.GridTileToLocal(spawnTile);
+
+                foreach (var entry in EntitySpawnCollection.GetSpawns(mobGroup.Entries, random))
+                {
+                    _entManager.SpawnEntity(entry, spawnPosition);
+                }
+
+                await SuspendIfOutOfTime();
+                break;
+            }
+        }
+    }
+
+    #endregion
+}
index 6eb228fbd88dae71ae1cd918447d54d5b18abde3..92ba6ee31d36c7db34e3146b92f90839bd4137d6 100644 (file)
@@ -6,4 +6,4 @@ namespace Content.Server.Shuttles.Events;
 /// Raised when <see cref="ShuttleSystem.FasterThanLight"/> has completed FTL Travel.
 /// </summary>
 [ByRefEvent]
-public readonly record struct FTLCompletedEvent;
+public readonly record struct FTLCompletedEvent(EntityUid Entity, EntityUid MapUid);
diff --git a/Content.Server/Shuttles/Events/FTLRequestEvent.cs b/Content.Server/Shuttles/Events/FTLRequestEvent.cs
new file mode 100644 (file)
index 0000000..7105407
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Server.Shuttles.Events;
+
+/// <summary>
+/// Raised by a shuttle when it has requested an FTL.
+/// </summary>
+[ByRefEvent]
+public record struct FTLRequestEvent(EntityUid MapUid);
index 091559163f3258650ab65c3b84cb80a33da08331..965da7f0c5e9cefecb0cfbe9c664bfdcbe8cd52f 100644 (file)
@@ -6,4 +6,4 @@ namespace Content.Server.Shuttles.Events;
 /// Raised when a shuttle has moved to FTL space.
 /// </summary>
 [ByRefEvent]
-public readonly record struct FTLStartedEvent(EntityUid? FromMapUid, Matrix3 FTLFrom, Angle FromRotation);
+public readonly record struct FTLStartedEvent(EntityUid Entity, EntityCoordinates TargetCoordinates, EntityUid? FromMapUid, Matrix3 FTLFrom, Angle FromRotation);
index 9c6de9106f4fe7217fabda5c63da7606b2a9b457..a9b3be6393f67a114a976bd938fd6ce73df4c0d1 100644 (file)
@@ -150,6 +150,8 @@ public sealed partial class ShuttleSystem
         hyperspace.Dock = false;
         hyperspace.PriorityTag = priorityTag;
         _console.RefreshShuttleConsoles();
+        var ev = new FTLRequestEvent(_mapManager.GetMapEntityId(coordinates.ToMap(EntityManager, _transform).MapId));
+        RaiseLocalEvent(shuttleUid, ref ev, true);
     }
 
     /// <summary>
@@ -249,8 +251,10 @@ public sealed partial class ShuttleSystem
 
                     SetDockBolts(uid, true);
                     _console.RefreshShuttleConsoles(uid);
-                    var ev = new FTLStartedEvent(fromMapUid, fromMatrix, fromRotation);
-                    RaiseLocalEvent(uid, ref ev);
+                    var target = comp.TargetUid != null ? new EntityCoordinates(comp.TargetUid.Value, Vector2.Zero) : comp.TargetCoordinates;
+
+                    var ev = new FTLStartedEvent(uid, target, fromMapUid, fromMatrix, fromRotation);
+                    RaiseLocalEvent(uid, ref ev, true);
 
                     if (comp.TravelSound != null)
                     {
@@ -344,7 +348,7 @@ public sealed partial class ShuttleSystem
                     comp.Accumulator += FTLCooldown;
                     _console.RefreshShuttleConsoles(uid);
                     _mapManager.SetMapPaused(mapId, false);
-                    var ftlEvent = new FTLCompletedEvent();
+                    var ftlEvent = new FTLCompletedEvent(uid, _mapManager.GetMapEntityId(mapId));
                     RaiseLocalEvent(uid, ref ftlEvent, true);
                     break;
                 case FTLState.Cooldown:
@@ -499,6 +503,7 @@ public sealed partial class ShuttleSystem
     public bool TryFTLProximity(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
     {
         if (!Resolve(targetUid, ref targetXform) ||
+            targetXform.GridUid == null ||
             targetXform.MapUid == null ||
             !targetXform.MapUid.Value.IsValid() ||
             !Resolve(shuttleUid, ref xform))
@@ -592,6 +597,23 @@ public sealed partial class ShuttleSystem
             spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
         }
 
+        // TODO: This is pretty crude for multiple landings.
+        if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid.Value))
+        {
+            var minRadius = (MathF.Max(targetAABB.Width, targetAABB.Height) + MathF.Max(shuttleAABB.Width, shuttleAABB.Height)) / 2f;
+            spawnPos = targetAABB.Center + _random.NextVector2(minRadius, minRadius + 64f);
+        }
+        else if (shuttleBody != null)
+        {
+            var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform, xformQuery);
+            var transform = new Transform(targetPos, targetRot);
+            spawnPos = Robust.Shared.Physics.Transform.Mul(transform, -shuttleBody.LocalCenter);
+        }
+        else
+        {
+            spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
+        }
+
         xform.Coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos);
 
         if (!HasComp<MapComponent>(targetXform.GridUid))
index 9b8d8ca430ba67f8f52e5cf7ffadca77e5de3f7a..c4ef905835cfc25c3c1baa2ccc8ad2b7c9a007d4 100644 (file)
@@ -1,6 +1,10 @@
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Noise;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
 namespace Content.Shared.Parallax.Biomes;
 
@@ -13,12 +17,22 @@ public sealed partial class BiomeComponent : Component
     [AutoNetworkedField]
     public int Seed;
 
-    [ViewVariables(VVAccess.ReadWrite),
-     DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<BiomePrototype>))]
+    /// <summary>
+    /// The underlying entity, decal, and tile layers for the biome.
+    /// </summary>
+    [DataField("layers")]
     [AutoNetworkedField]
-    public string BiomePrototype = "Grasslands";
+    public List<IBiomeLayer> Layers = new();
 
-    // TODO: Need to flag tiles as not requiring custom data anymore, e.g. if we spawn an ent and don't unspawn it.
+    /// <summary>
+    /// Templates to use for <see cref="Layers"/>. Optional as this can be set elsewhere.
+    /// </summary>
+    /// <remarks>
+    /// This is really just here for prototype reload support.
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadWrite),
+     DataField("template", customTypeSerializer: typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
+    public string? Template;
 
     /// <summary>
     /// If we've already generated a tile and couldn't deload it then we won't ever reload it in future.
@@ -42,9 +56,16 @@ public sealed partial class BiomeComponent : Component
     [DataField("loadedChunks")]
     public readonly HashSet<Vector2i> LoadedChunks = new();
 
+    #region Markers
+
     /// <summary>
-    /// Are we currently in the process of generating?
-    /// Used to flag modified tiles without callers having to deal with it.
+    /// Track what markers we've loaded already to avoid double-loading.
     /// </summary>
-    public bool Generating = false;
+    [DataField("loadedMarkers", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<Vector2i>, BiomeMarkerLayerPrototype>))]
+    public readonly Dictionary<string, HashSet<Vector2i>> LoadedMarkers = new();
+
+    [DataField("markerLayers", customTypeSerializer: typeof(PrototypeIdListSerializer<BiomeMarkerLayerPrototype>))]
+    public List<string> MarkerLayers = new();
+
+    #endregion
 }
diff --git a/Content.Shared/Parallax/Biomes/BiomePrototype.cs b/Content.Shared/Parallax/Biomes/BiomePrototype.cs
deleted file mode 100644 (file)
index 430c8a1..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-using Content.Shared.Decals;
-using Content.Shared.Maps;
-using Robust.Shared.Noise;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Parallax.Biomes;
-
-[Prototype("biome")]
-public sealed class BiomePrototype : IPrototype
-{
-    [IdDataField] public string ID { get; } = default!;
-
-    [DataField("desc")]
-    public string Description = string.Empty;
-
-    [DataField("layers")]
-    public List<IBiomeLayer> Layers = new();
-}
-
-[ImplicitDataDefinitionForInheritors]
-public interface IBiomeLayer
-{
-    /// <summary>
-    /// Seed is used an offset from the relevant BiomeComponent's seed.
-    /// </summary>
-    FastNoiseLite Noise { get; }
-
-    /// <summary>
-    /// Threshold for this layer to be present. If set to 0 forces it for every tile.
-    /// </summary>
-    float Threshold { get; }
-}
-
-public sealed class BiomeTileLayer : IBiomeLayer
-{
-    [DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
-
-    /// <inheritdoc/>
-    [DataField("threshold")]
-    public float Threshold { get; } = 0.5f;
-
-    /// <summary>
-    /// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
-    /// </summary>
-    [DataField("variants")]
-    public List<byte>? Variants = null;
-
-    [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    public string Tile = string.Empty;
-}
-
-/// <summary>
-/// Handles actual objects such as decals and entities.
-/// </summary>
-public interface IBiomeWorldLayer : IBiomeLayer
-{
-    /// <summary>
-    /// What tiles we're allowed to spawn on, real or biome.
-    /// </summary>
-    List<string> AllowedTiles { get; }
-}
-
-public sealed class BiomeDecalLayer : IBiomeWorldLayer
-{
-    /// <inheritdoc/>
-    [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
-    public List<string> AllowedTiles { get; } = new();
-
-    /// <summary>
-    /// Divide each tile up by this amount.
-    /// </summary>
-    [DataField("divisions")]
-    public float Divisions = 1f;
-
-    [DataField("noise")]
-    public FastNoiseLite Noise { get; } = new(0);
-
-    /// <inheritdoc/>
-    [DataField("threshold")]
-    public float Threshold { get; } = 0.8f;
-
-    [DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DecalPrototype>))]
-    public List<string> Decals = new();
-}
-
-public sealed class BiomeEntityLayer : IBiomeWorldLayer
-{
-    /// <inheritdoc/>
-    [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
-    public List<string> AllowedTiles { get; } = new();
-
-    [DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
-
-    /// <inheritdoc/>
-    [DataField("threshold")]
-    public float Threshold { get; } = 0.5f;
-
-    [DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
-    public List<string> Entities = new();
-}
diff --git a/Content.Shared/Parallax/Biomes/BiomeTemplatePrototype.cs b/Content.Shared/Parallax/Biomes/BiomeTemplatePrototype.cs
new file mode 100644 (file)
index 0000000..74a98f6
--- /dev/null
@@ -0,0 +1,17 @@
+using Content.Shared.Parallax.Biomes.Layers;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Parallax.Biomes;
+
+/// <summary>
+/// A preset group of biome layers to be used for a <see cref="BiomeComponent"/>
+/// </summary>
+[Prototype("biomeTemplate")]
+public sealed class BiomeTemplatePrototype : IPrototype
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("layers")]
+    public List<IBiomeLayer> Layers = new();
+}
diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeDecalLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeDecalLayer.cs
new file mode 100644 (file)
index 0000000..07e674c
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.Decals;
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[Serializable, NetSerializable]
+public sealed class BiomeDecalLayer : IBiomeWorldLayer
+{
+    /// <inheritdoc/>
+    [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
+    public List<string> AllowedTiles { get; } = new();
+
+    /// <summary>
+    /// Divide each tile up by this amount.
+    /// </summary>
+    [DataField("divisions")]
+    public float Divisions = 1f;
+
+    [DataField("noise")]
+    public FastNoiseLite Noise { get; } = new(0);
+
+    /// <inheritdoc/>
+    [DataField("threshold")]
+    public float Threshold { get; } = 0.8f;
+
+    /// <inheritdoc/>
+    [DataField("invert")] public bool Invert { get; } = false;
+
+    [DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DecalPrototype>))]
+    public List<string> Decals = new();
+}
diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeDummyLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeDummyLayer.cs
new file mode 100644 (file)
index 0000000..5395ebf
--- /dev/null
@@ -0,0 +1,18 @@
+using Robust.Shared.Noise;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+/// <summary>
+/// Dummy layer that specifies a marker to be replaced by external code.
+/// For example if they wish to add their own layers at specific points across different templates.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class BiomeDummyLayer : IBiomeLayer
+{
+    [DataField("id", required: true)] public string ID = string.Empty;
+
+    public FastNoiseLite Noise { get; } = new();
+    public float Threshold { get; }
+    public bool Invert { get; }
+}
diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeEntityLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeEntityLayer.cs
new file mode 100644 (file)
index 0000000..caf2fef
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[Serializable, NetSerializable]
+public sealed class BiomeEntityLayer : IBiomeWorldLayer
+{
+    /// <inheritdoc/>
+    [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
+    public List<string> AllowedTiles { get; } = new();
+
+    [DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
+
+    /// <inheritdoc/>
+    [DataField("threshold")]
+    public float Threshold { get; } = 0.5f;
+
+    /// <inheritdoc/>
+    [DataField("invert")] public bool Invert { get; } = false;
+
+    [DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
+    public List<string> Entities = new();
+}
diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs
new file mode 100644 (file)
index 0000000..7b8aa81
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Maps;
+using Robust.Shared.Noise;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[Serializable, NetSerializable]
+public sealed class  BiomeTileLayer : IBiomeLayer
+{
+    [DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
+
+    /// <inheritdoc/>
+    [DataField("threshold")]
+    public float Threshold { get; } = 0.5f;
+
+    /// <inheritdoc/>
+    [DataField("invert")] public bool Invert { get; } = false;
+
+    /// <summary>
+    /// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
+    /// </summary>
+    [DataField("variants")]
+    public List<byte>? Variants = null;
+
+    [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
+    public string Tile = string.Empty;
+}
diff --git a/Content.Shared/Parallax/Biomes/Layers/IBiomeLayer.cs b/Content.Shared/Parallax/Biomes/Layers/IBiomeLayer.cs
new file mode 100644 (file)
index 0000000..3f81b55
--- /dev/null
@@ -0,0 +1,22 @@
+using Robust.Shared.Noise;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[ImplicitDataDefinitionForInheritors]
+public interface IBiomeLayer
+{
+    /// <summary>
+    /// Seed is used an offset from the relevant BiomeComponent's seed.
+    /// </summary>
+    FastNoiseLite Noise { get; }
+
+    /// <summary>
+    /// Threshold for this layer to be present. If set to 0 forces it for every tile.
+    /// </summary>
+    float Threshold { get; }
+
+    /// <summary>
+    /// Is the thresold inverted so we need to be lower than it.
+    /// </summary>
+    public bool Invert { get; }
+}
\ No newline at end of file
diff --git a/Content.Shared/Parallax/Biomes/Layers/IBiomeWorldLayer.cs b/Content.Shared/Parallax/Biomes/Layers/IBiomeWorldLayer.cs
new file mode 100644 (file)
index 0000000..92632e2
--- /dev/null
@@ -0,0 +1,12 @@
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+/// <summary>
+/// Handles actual objects such as decals and entities.
+/// </summary>
+public interface IBiomeWorldLayer : IBiomeLayer
+{
+    /// <summary>
+    /// What tiles we're allowed to spawn on, real or biome.
+    /// </summary>
+    List<string> AllowedTiles { get; }
+}
diff --git a/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs b/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs
new file mode 100644 (file)
index 0000000..e70f054
--- /dev/null
@@ -0,0 +1,28 @@
+using Content.Shared.Parallax.Biomes.Points;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Parallax.Biomes.Markers;
+
+[Prototype("biomeMarkerLayer")]
+public sealed class BiomeMarkerLayerPrototype : IBiomeMarkerLayer
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
+    public string Prototype = string.Empty;
+
+    /// <inheritdoc />
+    [DataField("radius")]
+    public float Radius { get; } = 12f;
+
+    /// <summary>
+    /// How many mobs to spawn in one group.
+    /// </summary>
+    [DataField("groupCount")]
+    public int GroupCount = 1;
+
+    /// <inheritdoc />
+    [DataField("size")]
+    public int Size { get; } = 64;
+}
diff --git a/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs b/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs
new file mode 100644 (file)
index 0000000..7c47cf9
--- /dev/null
@@ -0,0 +1,22 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Parallax.Biomes.Points;
+
+/// <summary>
+/// Specifies one-off marker points to be used. This could be for dungeon markers, mob markers, etc.
+/// These are run outside of the tile / decal / entity layers.
+/// </summary>
+public interface IBiomeMarkerLayer : IPrototype
+{
+    /// <summary>
+    /// Minimum radius between 2 points
+    /// </summary>
+    [DataField("radius")]
+    public float Radius { get; }
+
+    /// <summary>
+    /// How large the pre-generated points area is.
+    /// </summary>
+    [DataField("size")]
+    public int Size { get; }
+}
index 1e34e63cda5135d564e9432ed45a36e6dd8374be..9d08f9682ed28c0791a22e4336497a0f457e036d 100644 (file)
@@ -1,11 +1,10 @@
 using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Maps;
-using Robust.Shared.GameStates;
+using Content.Shared.Parallax.Biomes.Layers;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Noise;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
 using Robust.Shared.Utility;
 
 namespace Content.Shared.Parallax.Biomes;
@@ -72,7 +71,7 @@ public abstract class SharedBiomeSystem : EntitySystem
         throw new ArgumentOutOfRangeException();
     }
 
-    public bool TryGetBiomeTile(EntityUid uid, MapGridComponent grid, FastNoiseLite noise, Vector2i indices, [NotNullWhen(true)] out Tile? tile)
+    public bool TryGetBiomeTile(EntityUid uid, MapGridComponent grid, Vector2i indices, [NotNullWhen(true)] out Tile? tile)
     {
         if (grid.TryGetTileRef(indices, out var tileRef) && !tileRef.Tile.IsEmpty)
         {
@@ -86,14 +85,13 @@ public abstract class SharedBiomeSystem : EntitySystem
             return false;
         }
 
-        return TryGetBiomeTile(indices, ProtoManager.Index<BiomePrototype>(biome.BiomePrototype),
-            biome.Noise, grid, out tile);
+        return TryGetBiomeTile(indices, biome.Layers, biome.Noise, grid, out tile);
     }
 
     /// <summary>
     /// Tries to get the tile, real or otherwise, for the specified indices.
     /// </summary>
-    public bool TryGetBiomeTile(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
+    public bool TryGetBiomeTile(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
     {
         if (grid?.TryGetTileRef(indices, out var tileRef) == true && !tileRef.Tile.IsEmpty)
         {
@@ -103,16 +101,16 @@ public abstract class SharedBiomeSystem : EntitySystem
 
         var oldSeed = noise.GetSeed();
 
-        for (var i = prototype.Layers.Count - 1; i >= 0; i--)
+        for (var i = layers.Count - 1; i >= 0; i--)
         {
-            var layer = prototype.Layers[i];
+            var layer = layers[i];
 
             if (layer is not BiomeTileLayer tileLayer)
                 continue;
 
             SetNoise(noise, oldSeed, layer.Noise);
 
-            if (TryGetTile(indices, noise, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
+            if (TryGetTile(indices, noise, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
             {
                 noise.SetSeed(oldSeed);
                 return true;
@@ -127,9 +125,10 @@ public abstract class SharedBiomeSystem : EntitySystem
     /// <summary>
     /// Gets the underlying biome tile, ignoring any existing tile that may be there.
     /// </summary>
-    private bool TryGetTile(Vector2i indices, FastNoiseLite seed, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
+    private bool TryGetTile(Vector2i indices, FastNoiseLite seed, bool invert, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
     {
         var found = seed.GetNoise(indices.X, indices.Y);
+        found = invert ? found * -1 : found;
 
         if (found < threshold)
         {
@@ -159,10 +158,10 @@ public abstract class SharedBiomeSystem : EntitySystem
     /// <summary>
     /// Tries to get the relevant entity for this tile.
     /// </summary>
-    protected bool TryGetEntity(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid,
+    protected bool TryGetEntity(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent grid,
         [NotNullWhen(true)] out string? entity)
     {
-        if (!TryGetBiomeTile(indices, prototype, noise, grid, out var tileRef))
+        if (!TryGetBiomeTile(indices, layers, noise, grid, out var tileRef))
         {
             entity = null;
             return false;
@@ -171,13 +170,15 @@ public abstract class SharedBiomeSystem : EntitySystem
         var tileId = TileDefManager[tileRef.Value.TypeId].ID;
         var oldSeed = noise.GetSeed();
 
-        for (var i = prototype.Layers.Count - 1; i >= 0; i--)
+        for (var i = layers.Count - 1; i >= 0; i--)
         {
-            var layer = prototype.Layers[i];
+            var layer = layers[i];
 
             // Decals might block entity so need to check if there's one in front of us.
             switch (layer)
             {
+                case BiomeDummyLayer:
+                    continue;
                 case IBiomeWorldLayer worldLayer:
                     if (!worldLayer.AllowedTiles.Contains(tileId))
                         continue;
@@ -188,7 +189,9 @@ public abstract class SharedBiomeSystem : EntitySystem
             }
 
             SetNoise(noise, oldSeed, layer.Noise);
+            var invert = layer.Invert;
             var value = noise.GetNoise(indices.X, indices.Y);
+            value = invert ? value * -1 : value;
 
             if (value < layer.Threshold)
             {
@@ -215,10 +218,10 @@ public abstract class SharedBiomeSystem : EntitySystem
     /// <summary>
     /// Tries to get the relevant decals for this tile.
     /// </summary>
-    public bool TryGetDecals(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid,
+    public bool TryGetDecals(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent grid,
         [NotNullWhen(true)] out List<(string ID, Vector2 Position)>? decals)
     {
-        if (!TryGetBiomeTile(indices, prototype, noise, grid, out var tileRef))
+        if (!TryGetBiomeTile(indices, layers, noise, grid, out var tileRef))
         {
             decals = null;
             return false;
@@ -227,13 +230,15 @@ public abstract class SharedBiomeSystem : EntitySystem
         var tileId = TileDefManager[tileRef.Value.TypeId].ID;
         var oldSeed = noise.GetSeed();
 
-        for (var i = prototype.Layers.Count - 1; i >= 0; i--)
+        for (var i = layers.Count - 1; i >= 0; i--)
         {
-            var layer = prototype.Layers[i];
+            var layer = layers[i];
 
             // Entities might block decal so need to check if there's one in front of us.
             switch (layer)
             {
+                case BiomeDummyLayer:
+                    continue;
                 case IBiomeWorldLayer worldLayer:
                     if (!worldLayer.AllowedTiles.Contains(tileId))
                         continue;
@@ -244,11 +249,15 @@ public abstract class SharedBiomeSystem : EntitySystem
             }
 
             SetNoise(noise, oldSeed, layer.Noise);
+            var invert = layer.Invert;
 
             // Check if the other layer should even render, if not then keep going.
             if (layer is not BiomeDecalLayer decalLayer)
             {
-                if (noise.GetNoise(indices.X, indices.Y) < layer.Threshold)
+                var value = noise.GetNoise(indices.X, indices.Y);
+                value = invert ? value * -1 : value;
+
+                if (value < layer.Threshold)
                     continue;
 
                 decals = null;
@@ -264,6 +273,7 @@ public abstract class SharedBiomeSystem : EntitySystem
                 {
                     var index = new Vector2(indices.X + x * 1f / decalLayer.Divisions, indices.Y + y * 1f / decalLayer.Divisions);
                     var decalValue = noise.GetNoise(index.X, index.Y);
+                    decalValue = invert ? decalValue * -1 : decalValue;
 
                     if (decalValue < decalLayer.Threshold)
                         continue;
index a1c51f6e8b54ad7202a908ba3d9c3539988ea7db..6aef0c1bdf5712828d1c0b146827df957061daff 100644 (file)
@@ -2,6 +2,13 @@ namespace Content.Shared.Procedural;
 
 public sealed class Dungeon
 {
+    /// <summary>
+    /// Starting position used to generate the dungeon from.
+    /// </summary>
+    public Vector2i Position;
+
+    public Vector2i Center;
+
     public List<DungeonRoom> Rooms = new();
 
     /// <summary>
diff --git a/Content.Shared/Procedural/Loot/BiomeTemplateLoot.cs b/Content.Shared/Procedural/Loot/BiomeTemplateLoot.cs
new file mode 100644 (file)
index 0000000..b4972e5
--- /dev/null
@@ -0,0 +1,13 @@
+using Content.Shared.Parallax.Biomes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Procedural.Loot;
+
+/// <summary>
+/// Adds a biome template layer for dungeon loot.
+/// </summary>
+public sealed class BiomeTemplateLoot : IDungeonLoot
+{
+    [DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
+    public string Prototype = string.Empty;
+}
diff --git a/Content.Shared/Procedural/Loot/ClusterLoot.cs b/Content.Shared/Procedural/Loot/ClusterLoot.cs
deleted file mode 100644 (file)
index ab11f33..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Procedural.Loot;
-
-/// <summary>
-/// Spawns loot at points in the specified rooms
-/// </summary>
-public sealed class ClusterLoot : IDungeonLoot
-{
-    /// <summary>
-    /// Minimum spawns in a cluster.
-    /// </summary>
-    [DataField("minCluster")]
-    public int MinClusterAmount;
-
-    /// <summary>
-    /// Maximum spawns in a cluster.
-    /// </summary>
-    [DataField("maxCluster")] public int MaxClusterAmount;
-
-    /// <summary>
-    /// Amount to spawn for the entire loot.
-    /// </summary>
-    [DataField("max")]
-    public int Amount;
-
-    /// <summary>
-    /// Number of points to spawn.
-    /// </summary>
-    [DataField("points")] public int Points;
-
-    [DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string Prototype { get; } = string.Empty;
-}
diff --git a/Content.Shared/Procedural/Loot/DungeonClusterLoot.cs b/Content.Shared/Procedural/Loot/DungeonClusterLoot.cs
new file mode 100644 (file)
index 0000000..f1f4607
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Shared.Random;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Procedural.Loot;
+
+/// <summary>
+/// Spawns loot at points in the specified area inside of a dungeon room.
+/// </summary>
+public sealed class DungeonClusterLoot : IDungeonLoot
+{
+    /// <summary>
+    /// Spawns in a cluster.
+    /// </summary>
+    [DataField("clusterAmount")]
+    public int ClusterAmount = 1;
+
+    /// <summary>
+    /// Number of clusters to spawn.
+    /// </summary>
+    [DataField("clusters")] public int Points = 1;
+
+    [DataField("lootTable", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
+    public string Prototype { get; } = string.Empty;
+}
index 933e5e5929823c1747dc255c03ade8f77913c531..6b9cd32cd2a2f423438d068705629b551794121e 100644 (file)
@@ -3,5 +3,4 @@ namespace Content.Shared.Procedural.Loot;
 [ImplicitDataDefinitionForInheritors]
 public interface IDungeonLoot
 {
-    string Prototype { get; }
 }
index 642dd8c003f46694a98cdc94559f5e74a120b7c5..c485e15cf37745872d2649909ffab8d7ece08ac6 100644 (file)
@@ -1,4 +1,6 @@
+using Content.Shared.Salvage;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
 
 namespace Content.Shared.Procedural.Loot;
 
@@ -12,6 +14,12 @@ public sealed class SalvageLootPrototype : IPrototype
 
     [DataField("desc")] public string Description = string.Empty;
 
+    /// <summary>
+    /// Mission types this loot is not allowed to spawn for
+    /// </summary>
+    [DataField("blacklist")]
+    public List<SalvageMissionType> Blacklist = new();
+
     /// <summary>
     /// All of the loot rules
     /// </summary>
diff --git a/Content.Shared/Procedural/Rewards/BankReward.cs b/Content.Shared/Procedural/Rewards/BankReward.cs
deleted file mode 100644 (file)
index 08218ea..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Content.Shared.Procedural.Rewards;
-
-/// <summary>
-/// Payout to the station's bank account.
-/// </summary>
-public sealed class BankReward : ISalvageReward
-{
-    [DataField("amount")]
-    public int Amount = 0;
-}
diff --git a/Content.Shared/Procedural/Rewards/ISalvageReward.cs b/Content.Shared/Procedural/Rewards/ISalvageReward.cs
deleted file mode 100644 (file)
index da18b2f..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Shared.Procedural.Rewards;
-
-[ImplicitDataDefinitionForInheritors]
-public interface ISalvageReward
-{
-
-}
diff --git a/Content.Shared/Procedural/Rewards/SalvageRewardPrototype.cs b/Content.Shared/Procedural/Rewards/SalvageRewardPrototype.cs
deleted file mode 100644 (file)
index c8fd126..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Procedural.Rewards;
-
-/// <summary>
-/// Given after successful completion of a salvage mission.
-/// </summary>
-[Prototype("salvageReward")]
-public sealed class SalvageRewardPrototype : IPrototype
-{
-    [IdDataField] public string ID { get; } = string.Empty;
-
-    [DataField("reward", required: true)] public ISalvageReward Reward = default!;
-}
diff --git a/Content.Shared/Salvage/Expeditions/Extraction/SalvageExtraction.cs b/Content.Shared/Salvage/Expeditions/Extraction/SalvageExtraction.cs
deleted file mode 100644 (file)
index e637fe1..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Content.Shared.Salvage.Expeditions.Extraction;
-
-public sealed class SalvageExtraction : ISalvageMission
-{
-    /// <summary>
-    /// Minimum weight to be used for a wave.
-    /// </summary>
-    [DataField("minWaveWeight")] public float MinWaveWeight = 5;
-
-    /// <summary>
-    /// Minimum time between 2 waves. Roughly the end of one to the start of another.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("waveCooldown")]
-    public TimeSpan WaveCooldown = TimeSpan.FromSeconds(60);
-
-    /// <summary>
-    /// How much weight accumulates per second while the expedition is active.
-    /// </summary>
-    [DataField("weightAccumulator")]
-    public float WeightAccumulator = 0.1f;
-}
diff --git a/Content.Shared/Salvage/Expeditions/IFactionExpeditionConfig.cs b/Content.Shared/Salvage/Expeditions/IFactionExpeditionConfig.cs
deleted file mode 100644 (file)
index cd1186c..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Shared.Salvage.Expeditions;
-
-
-public interface IFactionExpeditionConfig
-{
-
-}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/ISalvageMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/ISalvageMod.cs
new file mode 100644 (file)
index 0000000..d911e4e
--- /dev/null
@@ -0,0 +1,11 @@
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+public interface ISalvageMod
+{
+    /// <summary>
+    /// Player-friendly version describing this modifier.
+    /// </summary>
+    string Description { get; }
+
+    float Cost { get; }
+}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageBiomeMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageBiomeMod.cs
new file mode 100644 (file)
index 0000000..a5887b0
--- /dev/null
@@ -0,0 +1,31 @@
+using Content.Shared.Parallax.Biomes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+/// <summary>
+/// Affects the biome to be used for salvage.
+/// </summary>
+[Prototype("salvageBiomeMod")]
+public sealed class SalvageBiomeMod : IPrototype, ISalvageMod
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+
+    /// <summary>
+    /// Is weather allowed to apply to this biome.
+    /// </summary>
+    [DataField("weather")]
+    public bool Weather = true;
+
+    [DataField("biome", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
+    public string? BiomePrototype;
+}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs
new file mode 100644 (file)
index 0000000..8a7ce1c
--- /dev/null
@@ -0,0 +1,29 @@
+using Content.Shared.Procedural;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+[Prototype("salvageDungeonMod")]
+public sealed class SalvageDungeonMod : IPrototype, ISalvageMod
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    [DataField("proto", customTypeSerializer:typeof(PrototypeIdSerializer<DungeonConfigPrototype>))]
+    public string Proto = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+
+    /// <summary>
+    /// Biomes this dungeon can occur in.
+    /// </summary>
+    [DataField("biomeMods", customTypeSerializer:typeof(PrototypeIdListSerializer<SalvageBiomeMod>))]
+    public List<string>? BiomeMods;
+}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageLightMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageLightMod.cs
new file mode 100644 (file)
index 0000000..7393bf8
--- /dev/null
@@ -0,0 +1,26 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+[Prototype("salvageLightMod")]
+public sealed class SalvageLightMod : IPrototype, ISalvageMod
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+
+    [DataField("color", required: true)] public Color? Color;
+
+    /// <summary>
+    /// Biomes that this color applies to.
+    /// </summary>
+    [DataField("biomes", customTypeSerializer: typeof(PrototypeIdListSerializer<SalvageBiomeMod>))]
+    public List<string>? Biomes;
+}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageMod.cs
new file mode 100644 (file)
index 0000000..d353fac
--- /dev/null
@@ -0,0 +1,20 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+/// <summary>
+/// Generic modifiers with no additional data
+/// </summary>
+[Prototype("salvageMod")]
+public sealed class SalvageMod : IPrototype, ISalvageMod
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageTimeMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageTimeMod.cs
new file mode 100644 (file)
index 0000000..4f1ab80
--- /dev/null
@@ -0,0 +1,23 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+[Prototype("salvageTimeMod")]
+public sealed class SalvageTimeMod : IPrototype, ISalvageMod
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+
+    [DataField("minDuration")]
+    public int MinDuration = 600;
+
+    [DataField("maxDuration")]
+    public int MaxDuration = 660;
+}
diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs
new file mode 100644 (file)
index 0000000..7889e8b
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Shared.Parallax.Biomes;
+using Content.Shared.Weather;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+[Prototype("salvageWeatherMod")]
+public sealed class SalvageWeatherMod : IPrototype, ISalvageMod
+{
+    [IdDataField] public string ID { get; } = default!;
+
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+
+    [DataField("weather", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<WeatherPrototype>))]
+    public string WeatherPrototype = string.Empty;
+
+    /// <summary>
+    /// Whitelist for biomes. If empty assumed any allowed.
+    /// </summary>
+    [DataField("biomes", customTypeSerializer:typeof(PrototypeIdListSerializer<BiomeTemplatePrototype>))]
+    public List<string> Biomes = new();
+}
index 03184aca7057a3d1abe69806a51ad2bba838953b..5852838285303dfe6349f5b48471eca442a344db 100644 (file)
@@ -1,19 +1,28 @@
+using Content.Shared.Salvage.Expeditions.Modifiers;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
 
 namespace Content.Shared.Salvage.Expeditions;
 
 [Prototype("salvageFaction")]
-public sealed class SalvageFactionPrototype : IPrototype
+public sealed class SalvageFactionPrototype : IPrototype, ISalvageMod
 {
     [IdDataField] public string ID { get; } = default!;
 
+    [DataField("desc")] public string Description { get; } = string.Empty;
+
+    /// <summary>
+    /// Cost for difficulty modifiers.
+    /// </summary>
+    [DataField("cost")]
+    public float Cost { get; } = 0f;
+
     [ViewVariables(VVAccess.ReadWrite), DataField("groups", required: true)]
     public List<SalvageMobGroup> MobGroups = default!;
 
     /// <summary>
-    /// Per expedition type data for this faction.
+    /// Miscellaneous data for factions.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("configs", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<IFactionExpeditionConfig, SalvageExpeditionPrototype>))]
-    public Dictionary<string, IFactionExpeditionConfig> Configs = new();
+    [ViewVariables(VVAccess.ReadWrite), DataField("configs")]
+    public Dictionary<string, string> Configs = new();
 }
diff --git a/Content.Shared/Salvage/Expeditions/Structure/SalvageStructure.cs b/Content.Shared/Salvage/Expeditions/Structure/SalvageStructure.cs
deleted file mode 100644 (file)
index 052c6f6..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Content.Shared.Salvage.Expeditions.Structure;
-
-/// <summary>
-/// Destroy the specified number of structures to finish the expedition.
-/// </summary>
-[DataDefinition]
-public sealed class SalvageStructure : ISalvageMission
-{
-    [DataField("desc")]
-    public string Description = string.Empty;
-
-    [ViewVariables(VVAccess.ReadWrite), DataField("minStructures")]
-    public int MinStructures = 3;
-
-    [ViewVariables(VVAccess.ReadWrite), DataField("maxStructures")]
-    public int MaxStructures = 5;
-}
diff --git a/Content.Shared/Salvage/Expeditions/Structure/SalvageStructureFaction.cs b/Content.Shared/Salvage/Expeditions/Structure/SalvageStructureFaction.cs
deleted file mode 100644 (file)
index 3193a1d..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Shared.Salvage.Expeditions.Structure;
-
-/// <summary>
-/// Per-faction config for Salvage Structure expeditions.
-/// </summary>
-[DataDefinition]
-public sealed class SalvageStructureFaction : IFactionExpeditionConfig
-{
-    /// <summary>
-    /// Entity prototype of the structures to destroy.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField("spawn", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string Spawn = default!;
-
-    /// <summary>
-    /// How many groups of mobs to spawn.
-    /// </summary>
-    [DataField("groupCount")]
-    public int Groups = 5;
-}
diff --git a/Content.Shared/Salvage/ISalvageMission.cs b/Content.Shared/Salvage/ISalvageMission.cs
deleted file mode 100644 (file)
index b1f08b4..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Content.Shared.Salvage;
-
-public interface ISalvageMission {}
\ No newline at end of file
diff --git a/Content.Shared/Salvage/SalvageExpeditionPrototype.cs b/Content.Shared/Salvage/SalvageExpeditionPrototype.cs
deleted file mode 100644 (file)
index eec3cc8..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-using Content.Shared.Dataset;
-using Content.Shared.Parallax.Biomes;
-using Content.Shared.Procedural;
-using Content.Shared.Procedural.Loot;
-using Content.Shared.Procedural.Rewards;
-using Content.Shared.Random;
-using Content.Shared.Salvage.Expeditions;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Salvage;
-
-[Prototype("salvageExpedition")]
-public sealed class SalvageExpeditionPrototype : IPrototype
-{
-    [IdDataField] public string ID { get; } = default!;
-
-    /// <summary>
-    /// Naming scheme for the FTL marker.
-    /// </summary>
-    [DataField("nameProto", customTypeSerializer:typeof(PrototypeIdSerializer<DatasetPrototype>))]
-    public string NameProto = "names_borer";
-
-    /// <summary>
-    /// Biome to generate the dungeon.
-    /// </summary>
-    [DataField("biome", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<BiomePrototype>))]
-    public string Biome = string.Empty;
-
-    /// <summary>
-    /// Player-friendly description for the console.
-    /// </summary>
-    [DataField("desc")]
-    public string Description = string.Empty;
-
-    [DataField("difficultyRating")]
-    public DifficultyRating DifficultyRating = DifficultyRating.Minor;
-
-    // TODO: Make these modifiers but also add difficulty modifiers.
-    [DataField("light")]
-    public Color Light = Color.Black;
-
-    [DataField("temperature")]
-    public float Temperature = 293.15f;
-
-    [DataField("expedition", required: true)]
-    public ISalvageMission Mission = default!;
-
-    [DataField("minDuration")]
-    public TimeSpan MinDuration = TimeSpan.FromSeconds(9 * 60);
-
-    [DataField("maxDuration")]
-    public TimeSpan MaxDuration = TimeSpan.FromSeconds(12 * 60);
-
-    /// <summary>
-    /// Available factions for selection for this mission prototype.
-    /// </summary>
-    [DataField("factions", customTypeSerializer:typeof(PrototypeIdListSerializer<SalvageFactionPrototype>))]
-    public List<string> Factions = new();
-
-    [DataField("dungeonConfig", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<DungeonConfigPrototype>))]
-    public string DungeonConfigPrototype = string.Empty;
-
-    [DataField("reward", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
-    public string Reward = string.Empty;
-
-    /// <summary>
-    /// Possible loot prototypes available for this expedition.
-    /// This spawns during the mission and is not tied to completion.
-    /// </summary>
-    [DataField("loot", customTypeSerializer: typeof(PrototypeIdListSerializer<WeightedRandomPrototype>))]
-    public List<string> Loots = new();
-
-    [DataField("dungeonPosition")]
-    public Vector2i DungeonPosition = new(80, -25);
-}
-
-[Serializable, NetSerializable]
-public enum DifficultyRating : byte
-{
-    None,
-    Minor,
-    Moderate,
-    Hazardous,
-    Extreme,
-}
-
index 680a71090cc16dff12b4b0bc65da32295d2e6ee2..18247470ff8e7afd58fee5c9a0706ac7d752f76e 100644 (file)
@@ -1,7 +1,8 @@
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Shared.Salvage;
 
@@ -10,13 +11,15 @@ public sealed class SalvageExpeditionConsoleState : BoundUserInterfaceState
 {
     public TimeSpan NextOffer;
     public bool Claimed;
+    public bool Cooldown;
     public ushort ActiveMission;
-    public List<SalvageMission> Missions;
+    public List<SalvageMissionParams> Missions;
 
-    public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, ushort activeMission, List<SalvageMission> missions)
+    public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, bool cooldown, ushort activeMission, List<SalvageMissionParams> missions)
     {
         NextOffer = nextOffer;
         Claimed = claimed;
+        Cooldown = cooldown;
         ActiveMission = activeMission;
         Missions = missions;
     }
@@ -49,6 +52,12 @@ public sealed class SalvageExpeditionDataComponent : Component
     [ViewVariables]
     public bool Claimed => ActiveMission != 0;
 
+    /// <summary>
+    /// Are we actively cooling down from the last salvage mission.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
+    public bool Cooldown = false;
+
     /// <summary>
     /// Nexy time salvage missions are offered.
     /// </summary>
@@ -56,7 +65,7 @@ public sealed class SalvageExpeditionDataComponent : Component
     public TimeSpan NextOffer;
 
     [ViewVariables]
-    public readonly Dictionary<ushort, SalvageMission> Missions = new();
+    public readonly Dictionary<ushort, SalvageMissionParams> Missions = new();
 
     [ViewVariables] public ushort ActiveMission;
 
@@ -64,24 +73,84 @@ public sealed class SalvageExpeditionDataComponent : Component
 }
 
 [Serializable, NetSerializable]
-public sealed record SalvageMission
+public sealed record SalvageMissionParams
 {
     [ViewVariables]
     public ushort Index;
 
-    [ViewVariables(VVAccess.ReadWrite), DataField("config", required: true, customTypeSerializer:typeof(SalvageExpeditionPrototype))]
-    public string Config = default!;
+    [ViewVariables(VVAccess.ReadWrite)]
+    public SalvageMissionType MissionType;
 
-    [ViewVariables] public TimeSpan Duration;
+    [ViewVariables(VVAccess.ReadWrite)] public int Seed;
 
-    [ViewVariables] public int Seed;
+    /// <summary>
+    /// Base difficulty for this mission.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)] public DifficultyRating Difficulty;
 }
 
-[Serializable, NetSerializable]
-public enum SalvageEnvironment : byte
+/// <summary>
+/// Created from <see cref="SalvageMissionParams"/>. Only needed for data the client also needs for mission
+/// display.
+/// </summary>
+public sealed record SalvageMission(
+    int Seed,
+    DifficultyRating Difficulty,
+    string Dungeon,
+    string Faction,
+    SalvageMissionType Mission,
+    string Biome,
+    Color? Color,
+    TimeSpan Duration,
+    Dictionary<string, int> Loot,
+    List<string> Modifiers)
 {
-    Invalid = 0,
-    Caves,
+    /// <summary>
+    /// Seed used for the mission.
+    /// </summary>
+    public readonly int Seed = Seed;
+
+    /// <summary>
+    /// Difficulty rating.
+    /// </summary>
+    public DifficultyRating Difficulty = Difficulty;
+
+    /// <summary>
+    /// <see cref="SalvageDungeonMod"/> to be used.
+    /// </summary>
+    public readonly string Dungeon = Dungeon;
+
+    /// <summary>
+    /// <see cref="SalvageFactionPrototype"/> to be used.
+    /// </summary>
+    public readonly string Faction = Faction;
+
+    /// <summary>
+    /// Underlying mission params that generated this.
+    /// </summary>
+    public readonly SalvageMissionType Mission = Mission;
+
+    /// <summary>
+    /// Biome to be used for the mission.
+    /// </summary>
+    public readonly string Biome = Biome;
+
+    /// <summary>
+    /// Lighting color to be used (AKA outdoor lighting).
+    /// </summary>
+    public readonly Color? Color = Color;
+
+    /// <summary>
+    /// Mission duration.
+    /// </summary>
+    public TimeSpan Duration = Duration;
+
+    public Dictionary<string, int> Loot = Loot;
+
+    /// <summary>
+    /// Modifiers (outside of the above) applied to the mission.
+    /// </summary>
+    public List<string> Modifiers = Modifiers;
 }
 
 [Serializable, NetSerializable]
index b66dd1375ba644b2c7c0083cf56680d14b55c29c..06e5d1faba4cc6e4e6d59bfd60378dae99bc1ace 100644 (file)
+using System.Linq;
 using Content.Shared.Dataset;
 using Content.Shared.Procedural.Loot;
-using Content.Shared.Procedural.Rewards;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
-using Content.Shared.Salvage.Expeditions.Structure;
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Salvage;
 
 public abstract class SharedSalvageSystem : EntitySystem
 {
+    [Dependency] private readonly ILocalizationManager _loc = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+
     public static readonly TimeSpan MissionCooldown = TimeSpan.FromMinutes(5);
-    public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(10);
+    public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(15);
+
+    #region Descriptions
 
-    public static float GetDifficultyModifier(DifficultyRating difficulty)
+    public string GetMissionDescription(SalvageMission mission)
     {
-        // These should reflect how many salvage staff are expected to be required for the mission.
-        switch (difficulty)
+        // Hardcoded in coooooz it's dynamic based on difficulty and I'm lazy.
+        switch (mission.Mission)
+        {
+            case SalvageMissionType.Mining:
+                // Taxation: , ("tax", $"{GetMiningTax(mission.Difficulty) * 100f:0}")
+                return Loc.GetString("salvage-expedition-desc-mining");
+            case SalvageMissionType.Destruction:
+                var proto = _proto.Index<SalvageFactionPrototype>(mission.Faction).Configs["DefenseStructure"];
+
+                return Loc.GetString("salvage-expedition-desc-structure",
+                    ("count", GetStructureCount(mission.Difficulty)),
+                    ("structure", _loc.GetEntityData(proto).Name));
+            default:
+                throw new NotImplementedException();
+        }
+    }
+
+    public float GetMiningTax(DifficultyRating baseRating)
+    {
+        return 0.6f + (int) baseRating * 0.05f;
+    }
+
+    /// <summary>
+    /// Gets the amount of structures to destroy.
+    /// </summary>
+    public int GetStructureCount(DifficultyRating baseRating)
+    {
+        return 1 + (int) baseRating * 2;
+    }
+
+    #endregion
+
+    public int GetDifficulty(DifficultyRating rating)
+    {
+        switch (rating)
         {
             case DifficultyRating.None:
-                return 1f;
+                return 1;
             case DifficultyRating.Minor:
-                return 1.5f;
+                return 2;
             case DifficultyRating.Moderate:
-                return 3f;
+                return 4;
             case DifficultyRating.Hazardous:
-                return 6f;
+                return 6;
             case DifficultyRating.Extreme:
-                return 10f;
+                return 8;
             default:
-                throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null);
+                throw new ArgumentOutOfRangeException(nameof(rating), rating, null);
         }
     }
 
+    /// <summary>
+    /// How many groups of mobs to spawn for a mission.
+    /// </summary>
+    public float GetSpawnCount(DifficultyRating difficulty)
+    {
+        return (int) difficulty * 2;
+    }
+
     public static string GetFTLName(DatasetPrototype dataset, int seed)
     {
         var random = new System.Random(seed);
         return $"{dataset.Values[random.Next(dataset.Values.Count)]}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
     }
 
-    public static string GetFaction(List<string> factions, int seed)
+    public SalvageMission GetMission(SalvageMissionType config, DifficultyRating difficulty, int seed)
     {
-        var adjustedSeed = new System.Random(seed + 1);
-        return factions[adjustedSeed.Next(factions.Count)];
+        // This is on shared to ensure the client display for missions and what the server generates are consistent
+        var rating = (float) GetDifficulty(difficulty);
+        // Don't want easy missions to have any negative modifiers but also want
+        // easy to be a 1 for difficulty.
+        rating -= 1f;
+        var rand = new System.Random(seed);
+        var faction = GetMod<SalvageFactionPrototype>(rand, ref rating);
+        var biome = GetMod<SalvageBiomeMod>(rand, ref rating);
+        var dungeon = GetDungeon(biome.ID, rand, ref rating);
+        var mods = new List<string>();
+
+        SalvageLightMod? light = null;
+
+        if (biome.BiomePrototype != null)
+        {
+            light = GetLight(biome.ID, rand, ref rating);
+            mods.Add(light.Description);
+        }
+
+        var time = GetMod<SalvageTimeMod>(rand, ref rating);
+        // Round the duration to nearest 15 seconds.
+        var exactDuration = time.MinDuration + (time.MaxDuration - time.MinDuration) * rand.NextFloat();
+        exactDuration = MathF.Round(exactDuration / 15f) * 15f;
+        var duration = TimeSpan.FromSeconds(exactDuration);
+
+        if (time.ID != "StandardTime")
+        {
+            mods.Add(time.Description);
+        }
+
+        var loots = GetLoot(config, _proto.EnumeratePrototypes<SalvageLootPrototype>().ToList(), GetDifficulty(difficulty), seed);
+        return new SalvageMission(seed, difficulty, dungeon.ID, faction.ID, config, biome.ID, light?.Color, duration, loots, mods);
     }
 
-    public static IEnumerable<SalvageLootPrototype> GetLoot(List<string> loots, int seed, IPrototypeManager protoManager)
+    public SalvageDungeonMod GetDungeon(string biome, System.Random rand, ref float rating)
     {
-        var adjustedSeed = new System.Random(seed + 2);
+        var mods = _proto.EnumeratePrototypes<SalvageDungeonMod>().ToList();
+        mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
+        rand.Shuffle(mods);
 
-        for (var i = 0; i < loots.Count; i++)
+        foreach (var mod in mods)
         {
-            var loot = loots[i];
-            var a = protoManager.Index<WeightedRandomPrototype>(loot);
-            var lootConfig = a.Pick(adjustedSeed);
-            yield return protoManager.Index<SalvageLootPrototype>(lootConfig);
+            if (mod.BiomeMods?.Contains(biome) == false ||
+                mod.Cost > rating)
+            {
+                continue;
+            }
+
+            rating -= (int) mod.Cost;
+
+            return mod;
         }
+
+        throw new InvalidOperationException();
     }
 
-    public static ISalvageReward GetReward(WeightedRandomPrototype proto, int seed, IPrototypeManager protoManager)
+    public SalvageLightMod GetLight(string biome, System.Random rand, ref float rating)
     {
-        var adjustedSeed = new System.Random(seed + 3);
-        var rewardProto = proto.Pick(adjustedSeed);
-        return protoManager.Index<SalvageRewardPrototype>(rewardProto).Reward;
+        var mods = _proto.EnumeratePrototypes<SalvageLightMod>().ToList();
+        mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
+        rand.Shuffle(mods);
+
+        foreach (var mod in mods)
+        {
+            if (mod.Biomes?.Contains(biome) == false || mod.Cost > rating)
+                continue;
+
+            rating -= mod.Cost;
+
+            return mod;
+        }
+
+        throw new InvalidOperationException();
     }
 
-    #region Structure
+    public T GetMod<T>(System.Random rand, ref float rating) where T : class, IPrototype, ISalvageMod
+    {
+        var mods = _proto.EnumeratePrototypes<T>().ToList();
+        mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
+        rand.Shuffle(mods);
+
+        foreach (var mod in mods)
+        {
+            if (mod.Cost > rating)
+                continue;
+
+            rating -= mod.Cost;
+
+            return mod;
+        }
+
+        throw new InvalidOperationException();
+    }
 
-    public static int GetStructureCount(SalvageStructure structure, int seed)
+    private Dictionary<string, int> GetLoot(SalvageMissionType mission, List<SalvageLootPrototype> loots, int count, int seed)
     {
-        var adjustedSeed = new System.Random(seed + 4);
-        return adjustedSeed.Next(structure.MinStructures, structure.MaxStructures + 1);
+        var results = new Dictionary<string, int>();
+        var adjustedSeed = new System.Random(seed + 2);
+
+        for (var i = 0; i < count; i++)
+        {
+            adjustedSeed.Shuffle(loots);
+
+            foreach (var loot in loots)
+            {
+                if (loot.Blacklist.Contains(mission))
+                    continue;
+
+                var weh = results.GetOrNew(loot.ID);
+                weh++;
+                results[loot.ID] = weh;
+                break;
+            }
+        }
+
+        return results;
     }
+}
 
-    #endregion
+[Serializable, NetSerializable]
+public enum SalvageMissionType : byte
+{
+    /// <summary>
+    /// No dungeon, just ore loot and random mob spawns.
+    /// </summary>
+    Mining,
+
+    /// <summary>
+    /// Destroy the specified structures in a dungeon.
+    /// </summary>
+    Destruction,
+}
+
+[Serializable, NetSerializable]
+public enum DifficultyRating : byte
+{
+    None,
+    Minor,
+    Moderate,
+    Hazardous,
+    Extreme,
 }
diff --git a/Resources/Locale/en-US/procedural/biome.ftl b/Resources/Locale/en-US/procedural/biome.ftl
new file mode 100644 (file)
index 0000000..d24ec7d
--- /dev/null
@@ -0,0 +1,6 @@
+cmd-biome_clear-desc = Clears a biome entirely
+cmd-biome_clear-help = biome_clear <biomecomponent>
+cmd-biome_addlayer-desc = Adds another biome layer
+cmd-biome_addlayer-help = biome_addlayer <mapid> <biometemplate> [seed offset]
+cmd-biome_addmarkerlayer-desc = Adds another biome marker layer
+cmd-biome_addmarkerlayer-help = biome_addmarkerlayer <mapid> <biomemarkerlayer>
diff --git a/Resources/Locale/en-US/procedural/expeditions.ftl b/Resources/Locale/en-US/procedural/expeditions.ftl
new file mode 100644 (file)
index 0000000..126f284
--- /dev/null
@@ -0,0 +1,34 @@
+salvage-expedition-structure-examine = This is a [color=#B02E26]destruction[/color] objective
+
+salvage-expedition-window-title = Salvage expeditions
+salvage-expedition-window-difficulty = Difficulty:
+salvage-expedition-window-details = Details:
+salvage-expedition-window-hostiles = Hostiles:
+salvage-expedition-window-duration = Duration:
+salvage-expedition-window-biome = Biome:
+salvage-expedition-window-modifiers = Modifiers:
+salvage-expedition-window-loot = Loot:
+salvage-expedition-window-none = N/A
+salvage-expedition-window-claimed = Claimed
+salvage-expedition-window-claim = Claim
+
+salvage-expedition-window-next = Next offer
+
+# Expedition descriptions
+salvage-expedition-desc-mining = Collect resources inside the area.
+#  You will be taxed {$tax}% of the resources collected.
+salvage-expedition-desc-structure = Destroy {$count} {$structure} inside the area.
+
+salvage-expedition-type-Mining = Mining
+salvage-expedition-type-Destruction = Destruction
+
+salvage-expedition-difficulty-None = None
+salvage-expedition-difficulty-Minor = Minor
+salvage-expedition-difficulty-Moderate = Moderate
+salvage-expedition-difficulty-Hazardous = Hazardous
+salvage-expedition-difficulty-Extreme = Extreme
+
+# Runner
+salvage-expedition-announcement-countdown-minutes = {$duration} minutes remaining to complete the expedition.
+salvage-expedition-announcement-countdown-seconds = {$duration} seconds remaining to complete the expedition.
+salvage-expedition-announcement-dungeon = Dungeon is located {$direction}.
index c5600b52a9667e65d582717f47abda244f6e4904..3799dc219f96e93e36865280dd76d1dc04b797d1 100644 (file)
@@ -1089,12 +1089,6 @@ entities:
   - pos: 0.5,0.5\r
     parent: 0\r
     type: Transform\r
-- uid: 85\r
-  type: PaperWrittenSalvageLoreMedium1PlasmaTrap\r
-  components:\r
-  - pos: 0.48327154,0.5698495\r
-    parent: 0\r
-    type: Transform\r
 - uid: 86\r
   type: ClothingEyesGlassesMeson\r
   components:\r
index 1aa25af3ef8d6a9925c47915db3c5059163a1c1e..2635fc634108a6e53c91989383bb30bb146a210c 100644 (file)
@@ -568,12 +568,6 @@ entities:
   - pos: -3.5,0.5\r
     parent: 0\r
     type: Transform\r
-- uid: 55\r
-  type: SalvageLorePaperGamingSpawner\r
-  components:\r
-  - pos: -1.5,-2.5\r
-    parent: 0\r
-    type: Transform\r
 - uid: 56\r
   type: SalvageMobSpawner75\r
   components:\r
diff --git a/Resources/Prototypes/Catalog/Fills/Paper/salvage_lore.yml b/Resources/Prototypes/Catalog/Fills/Paper/salvage_lore.yml
deleted file mode 100644 (file)
index 22494b0..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-# ---- SPECIFICS ----
-
-- type: entity
-  id: PaperWrittenSalvageLoreMedium1PlasmaTrap
-  noSpawn: true # keep this from spamming spawn sheet
-  suffix: "Salvage: Lore: Medium 1: Plasma Trap"
-  parent: Paper
-  components:
-  - type: Paper
-    content: book-text-plasma-trap
-# ---- GAMING ----
-
-- type: entity
-  name: Salvage Lore Paper Gaming Spawner
-  id: SalvageLorePaperGamingSpawner
-  parent: MarkerBase
-  components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Objects/Misc/bureaucracy.rsi
-          state: paper_words
-    - type: RandomSpawner
-      prototypes:
-        - PaperWrittenSalvageLoreGaming1
-        - PaperWrittenSalvageLoreGaming2
-        - PaperWrittenSalvageLoreGaming3
-        - PaperWrittenSalvageLoreGaming4
-      offset: 0.1
-
-- type: entity
-  id: PaperWrittenSalvageLoreGaming1
-  noSpawn: true # keep this from spamming spawn sheet
-  suffix: "Salvage: Lore: Gaming 1"
-  parent: Paper
-  components:
-  - type: Paper
-    content: book-text-gaming1
-
-- type: entity
-  id: PaperWrittenSalvageLoreGaming2
-  noSpawn: true # keep this from spamming spawn sheet
-  suffix: "Salvage: Lore: Gaming 2"
-  parent: Paper
-  components:
-  - type: Paper
-    content: book-text-gaming2
-
-- type: entity
-  id: PaperWrittenSalvageLoreGaming3
-  noSpawn: true # keep this from spamming spawn sheet
-  suffix: "Salvage: Lore: Gaming 3"
-  parent: Paper
-  components:
-  - type: Paper
-    content: book-text-gaming3
-
-- type: entity
-  id: PaperWrittenSalvageLoreGaming4
-  noSpawn: true # keep this from spamming spawn sheet
-  suffix: "Salvage: Lore: Gaming 4"
-  parent: Paper
-  components:
-  - type: Paper
-    content: book-text-gaming4
-# ----
index bfe7cfc64efbf6087504a9c500771f233a38e101..fde5e6d4fe52eb996a782cfacecb2e158deaa8fa 100644 (file)
     - type: ComputerBoard
       prototype: ComputerCargoShuttle
 
+- type: entity
+  parent: BaseComputerCircuitboard
+  id: SalvageExpeditionsComputerCircuitboard
+  name: salvage expeditions computer board
+  description: A computer printed circuit board for a salvage expeditions computer.
+  components:
+    - type: Sprite
+      state: cpu_supply
+    - type: ComputerBoard
+      prototype: ComputerSalvageExpedition
+
 - type: entity
   parent: BaseComputerCircuitboard
   id: CargoShuttleConsoleCircuitboard
index 8bbf94c43088e1540e69e6c9a057c516325debd7..0fd8d2c8118c0c0fb38fe854cdcd457a5356a81e 100644 (file)
     damageContainer: Inorganic
     damageModifierSet: StrongMetallic
 
+- type: entity
+  id: ComputerSalvageExpedition
+  parent: BaseComputer
+  name: salvage expeditions computer
+  description: Used take salvage missions.
+  components:
+    - type: Sprite
+      layers:
+        - map: ["computerLayerBody"]
+          state: computer
+        - map: ["computerLayerKeyboard"]
+          state: generic_keyboard
+        - map: [ "computerLayerScreen" ]
+          state: mining
+        - map: ["computerLayerKeys"]
+          state: tech_key
+    - type: Appearance
+    - type: GenericVisualizer
+      visuals:
+        enum.ComputerVisuals.Powered:
+          computerLayerScreen:
+            True: { visible: true, shader: unshaded }
+            False: { visible: false }
+          computerLayerKeys:
+            True: { visible: true, shader: unshaded }
+            False: { visible: true }
+    - type: SalvageExpeditionConsole
+    - type: ActivatableUI
+      key: enum.SalvageConsoleUiKey.Expedition
+    - type: ActivatableUIRequiresPower
+    - type: UserInterface
+      interfaces:
+        - key: enum.SalvageConsoleUiKey.Expedition
+          type: SalvageExpeditionConsoleBoundUserInterface
+    - type: Computer
+      board: SalvageExpeditionsComputerCircuitboard
+    - type: PointLight
+      radius: 1.5
+      energy: 1.6
+      color: "#b89f25"
+    - type: AccessReader
+      access: [["Salvage"]]
+
 - type: entity
   parent: BaseComputer
   id: ComputerSurveillanceCameraMonitor
index a1c5e647791aa57c2bf496604700d63fd2176799..d6f624a8986540e4589e6c12b30cf2e4703f07c4 100644 (file)
@@ -1,7 +1,6 @@
 - type: entity
   id: XenoWardingTower
   name: Xeno warding tower
-  description: a
   placement:
     mode: SnapgridCenter
     snap:
@@ -16,6 +15,7 @@
         Heat:
           collection:
             MeatLaserImpact
+    - type: Clickable
     - type: InteractionOutline
     - type: Sprite
       netsync: false
diff --git a/Resources/Prototypes/Procedural/biome_markers.yml b/Resources/Prototypes/Procedural/biome_markers.yml
new file mode 100644 (file)
index 0000000..2f386f6
--- /dev/null
@@ -0,0 +1,14 @@
+- type: biomeMarkerLayer
+  id: Lizards
+  proto: MobLizard
+  groupCount: 5
+
+# TODO: Needs to be more robust
+- type: biomeMarkerLayer
+  id: Xenos
+  proto: MobXeno
+
+
+#- type: biomeMarkerLayer
+#  id: Experiment
+#  proto: DungeonMarkerExperiment
diff --git a/Resources/Prototypes/Procedural/biome_ore_templates.yml b/Resources/Prototypes/Procedural/biome_ore_templates.yml
new file mode 100644 (file)
index 0000000..536b97e
--- /dev/null
@@ -0,0 +1,108 @@
+# Allowed
+#allowedTiles:
+#- FloorPlanetGrass
+#- FloorPlanetDirt
+#- FloorSnow
+#- FloorBasalt
+#- FloorAsteroidSand
+
+- type: biomeTemplate
+  id: OreTin
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: 0.90
+      allowedTiles:
+        - FloorPlanetGrass
+        - FloorPlanetDirt
+        - FloorSnow
+        - FloorBasalt
+        - FloorAsteroidSand
+      noise:
+        seed: 100
+        noiseType: OpenSimplex2
+        frequency: 0.04
+        fractalType: None
+      entities:
+        - WallRockTin
+
+# Medium value
+# Gold
+- type: biomeTemplate
+  id: OreGold
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: 0.95
+      allowedTiles:
+        - FloorPlanetGrass
+        - FloorPlanetDirt
+        - FloorSnow
+        - FloorBasalt
+        - FloorAsteroidSand
+      noise:
+        seed: 100
+        noiseType: OpenSimplex2
+        frequency: 0.04
+        fractalType: None
+      entities:
+        - WallRockGold
+
+# Silver
+- type: biomeTemplate
+  id: OreSilver
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: 0.95
+      allowedTiles:
+        - FloorPlanetGrass
+        - FloorPlanetDirt
+        - FloorSnow
+        - FloorBasalt
+        - FloorAsteroidSand
+      noise:
+        seed: 100
+        noiseType: OpenSimplex2
+        frequency: 0.05
+        fractalType: None
+      entities:
+        - WallRockSilver
+
+# High value
+# Plasma
+- type: biomeTemplate
+  id: OrePlasma
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: 0.99
+      allowedTiles:
+        - FloorPlanetGrass
+        - FloorPlanetDirt
+        - FloorSnow
+        - FloorBasalt
+        - FloorAsteroidSand
+      noise:
+        seed: 100
+        noiseType: OpenSimplex2
+        frequency: 0.04
+        fractalType: None
+      entities:
+        - WallRockPlasma
+
+# Uranium
+- type: biomeTemplate
+  id: OreUranium
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: 0.99
+      allowedTiles:
+        - FloorPlanetGrass
+        - FloorPlanetDirt
+        - FloorSnow
+        - FloorBasalt
+        - FloorAsteroidSand
+      noise:
+        seed: 100
+        noiseType: OpenSimplex2
+        frequency: 0.04
+        fractalType: None
+      entities:
+        - WallRockUranium
similarity index 91%
rename from Resources/Prototypes/biomes.yml
rename to Resources/Prototypes/Procedural/biome_templates.yml
index 625cad2d95ba33fb07830bea017b648907d65aac..f6144977e47772052cf81d697931fbb461b249c6 100644 (file)
@@ -1,8 +1,7 @@
 # Desert
 # TODO: Water in desert
-- type: biome
+- type: biomeTemplate
   id: LowDesert
-  desc: Desert
   layers:
     - !type:BiomeEntityLayer
       threshold: 0.95
@@ -33,6 +32,8 @@
         - FloorLowDesert
       entities:
         - AsteroidRock
+    - !type:BiomeDummyLayer
+      id: Loot
     # Fill layer
     - !type:BiomeTileLayer
       threshold: -1
@@ -48,9 +49,8 @@
         frequency: 0.1
 
 # Grass
-- type: biome
+- type: biomeTemplate
   id: Grasslands
-  desc: Grasslands
   layers:
     # Sparse vegetation
     - !type:BiomeDecalLayer
         cellularReturnType: Distance2
       entities:
         - WallRock
+    - !type:BiomeDummyLayer
+      id: Loot
     # Water
     - !type:BiomeEntityLayer
       allowedTiles:
         cellularReturnType: Distance2
 
 # Lava
-- type: biome
+- type: biomeTemplate
   id: Lava
-  desc: Lava
   layers:
     - !type:BiomeEntityLayer
       threshold: 0.9
         - FloorBasalt
       entities:
         - FloorLavaEntity
+    - !type:BiomeDummyLayer
+      id: Loot
     # Fill basalt
     - !type:BiomeTileLayer
       threshold: -1
       tile: FloorBasalt
 
 # Snow
-- type: biome
+- type: biomeTemplate
   id: Snow # Similar to Grasslands... but snow
   layers:
     # Sparse vegetation
         - FloraTreeSnow04
         - FloraTreeSnow05
         - FloraTreeSnow06
+    - !type:BiomeDummyLayer
+      id: Loot
     - !type:BiomeTileLayer
       threshold: -1.0
       tile: FloorSnow
         seed: 0
         frequency: 0.02
         fractalType: None
+
+# Caves
+- type: biomeTemplate
+  id: Caves
+  layers:
+    - !type:BiomeEntityLayer
+      threshold: -0.5
+      invert: true
+      noise:
+        seed: 0
+        noiseType: Perlin
+        fractalType: Ridged
+        octaves: 1
+        frequency: 0.1
+        gain: 0
+      allowedTiles:
+        - FloorAsteroidSand
+      entities:
+        - WallRock
+    - !type:BiomeDummyLayer
+      id: Loot
+    - !type:BiomeTileLayer
+      threshold: -1.0
+      tile: FloorAsteroidSand
index 63766a0693a0681b6ec47ff7db7d62eae2ccbac2..28bf1481cd2c7634117cc9b2fadb85f78ff1b3e9 100644 (file)
   id: SmallArea1
   size: 5,5
   rooms:
-    - 0,0,5,5
+    - 0,0,5,5
\ No newline at end of file
diff --git a/Resources/Prototypes/Procedural/salvage_factions.yml b/Resources/Prototypes/Procedural/salvage_factions.yml
new file mode 100644 (file)
index 0000000..06e088a
--- /dev/null
@@ -0,0 +1,16 @@
+- type: salvageFaction
+  id: Xenos
+  groups:
+    - entries:
+        - id: MobXeno
+          amount: 2
+          maxAmount: 3
+        - id: MobXenoDrone
+          amount: 1
+    - entries:
+        - id: MobXenoRavager
+          amount: 1
+      prob: 0.1
+  configs:
+    DefenseStructure: XenoWardingTower
+    Mining: Xenos
diff --git a/Resources/Prototypes/Procedural/salvage_loot.yml b/Resources/Prototypes/Procedural/salvage_loot.yml
new file mode 100644 (file)
index 0000000..a14722e
--- /dev/null
@@ -0,0 +1,94 @@
+# Loot tables
+#- type: weightedRandom
+#  id: SalvageLowValue
+#  weights:
+    # Common
+#    CrateSalvageAssortedGoodies: 1.0
+    # Uncommon
+    # TODO:
+    # Rare
+
+- type: weightedRandom
+  id: SalvageHighValue
+  weights:
+    # Common
+    CrateMaterialPlasteel: 1.0
+    CrateMaterialWood: 1.0
+    CrateMaterialPlastic: 1.0
+    CrateSalvageEquipment: 1.0
+    CrateMaterialSteel: 1.0
+    CrateMaterialGlass: 1.0
+    # Uncommon
+    SuperCapacitorStockPart: 0.25
+    PhasicScanningModuleStockPart: 0.25
+    PicoManipulatorStockPart: 0.25
+    UltraHighPowerMicroLaserStockPart: 0.25
+    SuperMatterBinStockPart: 0.25
+    # Rare
+    QuadraticCapacitorStockPart: 0.10
+    TriphasicScanningModuleStockPart: 0.10
+    FemtoManipulatorStockPart: 0.10
+    QuadUltraMicroLaserStockPart: 0.10
+    BluespaceMatterBinStockPart: 0.10
+
+# Crates
+#- type: salvageLoot
+#  id: LowValue
+#  desc: Commodities
+#  blacklist:
+#    - Mining
+#  loots:
+#    - !type:DungeonClusterLoot
+#      lootTable: SalvageLowValue
+#      clusters: 3
+#      clusterAmount: 3
+
+- type: salvageLoot
+  id: HighValue
+  desc: High-value commodities
+  blacklist:
+    - Mining
+  loots:
+    - !type:DungeonClusterLoot
+      lootTable: SalvageHighValue
+      clusters: 5
+      clusterAmount: 1
+
+# Ores
+# - Low value
+- type: salvageLoot
+  id: OreTin
+  desc: Veins of steel
+  loots:
+    - !type:BiomeTemplateLoot
+      proto: OreTin
+
+# - Medium value
+- type: salvageLoot
+  id: OreGold
+  desc: Veins of gold ore
+  loots:
+    - !type:BiomeTemplateLoot
+      proto: OreGold
+
+- type: salvageLoot
+  id: OreSilver
+  desc: Veins of silver ore
+  loots:
+    - !type:BiomeTemplateLoot
+      proto: OreSilver
+
+# - High value
+- type: salvageLoot
+  id: OrePlasma
+  desc: Veins of plasma ore
+  loots:
+    - !type:BiomeTemplateLoot
+      proto: OrePlasma
+
+- type: salvageLoot
+  id: OreUranium
+  desc: Veins of uranium ore
+  loots:
+    - !type:BiomeTemplateLoot
+      proto: OreUranium
diff --git a/Resources/Prototypes/Procedural/salvage_misc.yml b/Resources/Prototypes/Procedural/salvage_misc.yml
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Resources/Prototypes/Procedural/salvage_mods.yml b/Resources/Prototypes/Procedural/salvage_mods.yml
new file mode 100644 (file)
index 0000000..d9e36a9
--- /dev/null
@@ -0,0 +1,102 @@
+# Markers
+- type: entity
+  id: SalvageShuttleMarker
+  parent: FTLPoint
+
+# Biome mods -> at least 1 required
+- type: salvageBiomeMod
+  id: Grasslands
+  biome: Grasslands
+
+- type: salvageBiomeMod
+  id: Lava
+  cost: 2
+  biome: Lava
+
+- type: salvageBiomeMod
+  id: Snow
+  biome: Snow
+
+- type: salvageBiomeMod
+  id: Caves
+  cost: 1
+  biome: Caves
+
+#- type: salvageBiomeMod
+#  id: Space
+#  cost: 1
+#  weather: false
+#  biome: null
+
+# Temperature mods -> not required
+# Also whitelist it
+
+# Weather mods -> not required
+- type: salvageWeatherMod
+  id: SnowfallHeavy
+  weather: SnowfallHeavy
+  cost: 1
+
+- type: salvageWeatherMod
+  id: Rain
+  weather: Rain
+
+# Light mods -> required
+# At some stage with sub-biomes this will probably be moved onto the biome itself
+- type: salvageLightMod
+  id: Daylight
+  desc: Daylight
+  color: "#D8B059"
+  biomes:
+    - Grasslands
+
+- type: salvageLightMod
+  id: Lavalight
+  desc: Daylight
+  color: "#A34931"
+  biomes:
+    - Lava
+
+- type: salvageLightMod
+  id: Evening
+  desc: Evening
+  color: "#2b3143"
+
+- type: salvageLightMod
+  id: Night
+  desc: Night time
+  color: null
+  cost: 1
+
+# Time mods -> at least 1 required
+- type: salvageTimeMod
+  id: StandardTime
+
+- type: salvageTimeMod
+  id: RushTime
+  desc: Rush
+  minDuration: 480
+  maxDuration: 540
+  cost: 1
+
+# Misc mods
+- type: salvageMod
+  id: LongDistance
+  desc: Long distance
+
+# Dungeons
+#  For now just simple 1-dungeon setups
+- type: salvageDungeonMod
+  id: Experiment
+  proto: Experiment
+  biomeMods:
+    - Caves
+    #- LowDesert
+    - Snow
+    - Grasslands
+
+- type: salvageDungeonMod
+  id: LavaBrig
+  proto: LavaBrig
+  biomeMods:
+    - Lava