diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs index 67ee82aefb..ce3e556384 100644 --- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs +++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs @@ -128,7 +128,7 @@ namespace Content.Client.Lobby.UI _viewBox.AddChild(viewWest); _viewBox.AddChild(viewEast); _summaryLabel.Text = selectedCharacter.Summary; - EntitySystem.Get().LoadProfile(_previewDummy.Value, selectedCharacter); + _entityManager.System().LoadProfile(_previewDummy.Value, selectedCharacter); GiveDummyJobClothes(_previewDummy.Value, selectedCharacter); } } diff --git a/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml b/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml index 30a747d715..67280c34f9 100644 --- a/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml +++ b/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml @@ -1,11 +1,11 @@ ? 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(mission.Config); - var dungeonConfig = _prototype.Index(config.DungeonConfigPrototype); - var faction = SharedSalvageSystem.GetFaction(config.Factions, mission.Seed); - var factionConfig = _prototype.Index(faction); + var missionParams = state.Missions[i]; + var config = missionParams.MissionType; + var mission = _salvage.GetMission(missionParams.MissionType, missionParams.Difficulty, missionParams.Seed); - // 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(structureConfig.Spawn).Name} structures."; - break; - default: - throw new NotImplementedException(); - } - - // 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") }); + var biome = mission.Biome; + lBox.AddChild(new Label { - Text = _prototype.Index(config.Biome).Description, - FontColorOverride = StyleNano.NanoGold, - HorizontalAlignment = HAlignment.Left, - Margin = new Thickness(0f, 0f, 0f, 5f), - }); - - // Environment - lBox.AddChild(new Label - { - Text = "Environment:" - }); - - lBox.AddChild(new Label - { - Text = config.Description, + Text = Loc.GetString(_prototype.Index(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(config.Reward), mission.Seed, _prototype); - var difficulty = config.DifficultyRating; - var rewardDesc = string.Empty; + var mods = mission.Modifiers; - switch (salvageReward) + lBox.AddChild(new Label { - case BankReward bank: - rewardDesc = $"Bank payment of {(int) (bank.Amount * SharedSalvageSystem.GetDifficultyModifier(difficulty))}"; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - 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(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}"; } } diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 642b5d88d4..675fedc807 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -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 { diff --git a/Content.Server/Maps/PlanetCommand.cs b/Content.Server/Maps/PlanetCommand.cs index 5f1a9a5c12..902d25388b 100644 --- a/Content.Server/Maps/PlanetCommand.cs +++ b/Content.Server/Maps/PlanetCommand.cs @@ -53,7 +53,7 @@ public sealed class PlanetCommand : IConsoleCommand return; } - if (!_protoManager.HasIndex(args[1])) + if (!_protoManager.TryIndex(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(mapUid); - _entManager.System().SetPrototype(biome, args[1]); - _entManager.System().SetSeed(biome, _random.Next()); + var biomeSystem = _entManager.System(); + biomeSystem.SetSeed(biome, _random.Next()); + biomeSystem.SetTemplate(biome, biomeTemplate); _entManager.Dirty(biome); var gravity = _entManager.EnsureComponent(mapUid); @@ -106,7 +107,7 @@ public sealed class PlanetCommand : IConsoleCommand if (args.Length == 2) { - var options = _protoManager.EnumeratePrototypes() + var options = _protoManager.EnumeratePrototypes() .Select(o => new CompletionOption(o.ID, "Biome")); return CompletionResult.FromOptions(options); } diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs index d07aecd1ea..ad4cbf27d3 100644 --- a/Content.Server/NPC/HTN/HTNSystem.cs +++ b/Content.Server/NPC/HTN/HTNSystem.cs @@ -162,8 +162,9 @@ public sealed class HTNSystem : EntitySystem public void UpdateNPC(ref int count, int maxUpdates, float frameTime) { _planQueue.Process(); + var query = EntityQueryEnumerator(); - foreach (var (_, comp) in EntityQuery()) + 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(comp.Owner); + RemComp(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 index 0000000000..0a60a3f978 --- /dev/null +++ b/Content.Server/Parallax/BiomeSystem.Commands.cs @@ -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(_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(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(_mapManager.GetMapEntityId(mapId), out var biome)) + { + return; + } + + if (!_proto.TryIndex(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(proto: _proto), "Biome template"); + } + + if (args.Length == 3) + { + if (int.TryParse(args[0], out var mapInt)) + { + var mapId = new MapId(mapInt); + + if (TryComp(_mapManager.GetMapEntityId(mapId), out var biome)) + { + var results = new List(); + + 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(_mapManager.GetMapEntityId(mapId), out var biome)) + { + return; + } + + if (!_proto.HasIndex(args[1])) + { + return; + } + + biome.MarkerLayers.Add(args[1]); + } + + private CompletionResult AddMarkerLayerCallbackHelper(IConsoleShell shell, string[] args) + { + if (args.Length == 1) + { + return CompletionResult.FromHintOptions(CompletionHelper.Components(args[0], EntityManager), "Biome"); + } + + if (args.Length == 2) + { + return CompletionResult.FromHintOptions( + CompletionHelper.PrototypeIDs(proto: _proto), "Marker"); + } + + return CompletionResult.Empty; + } +} diff --git a/Content.Server/Parallax/BiomeSystem.cs b/Content.Server/Parallax/BiomeSystem.cs index 5733d73277..e7227c0045 100644 --- a/Content.Server/Parallax/BiomeSystem.cs +++ b/Content.Server/Parallax/BiomeSystem.cs @@ -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 _handledEntities = new(); private const float DefaultLoadRange = 16f; private float _loadRange = DefaultLoadRange; + + /// + /// Load area for chunks containing tiles, decals etc. + /// private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange); /// @@ -30,18 +44,41 @@ public sealed class BiomeSystem : SharedBiomeSystem /// private readonly Dictionary> _activeChunks = new(); + private readonly Dictionary>> _markerChunks = new(); + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBiomeStartup); SubscribeLocalEvent(OnBiomeMapInit); + SubscribeLocalEvent(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(); + + 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,15 +98,6 @@ public sealed class BiomeSystem : SharedBiomeSystem SetSeed(component, _random.Next()); } - public void SetPrototype(BiomeComponent component, string proto) - { - if (component.BiomePrototype == proto) - return; - - component.BiomePrototype = proto; - Dirty(component); - } - public void SetSeed(BiomeComponent component, int seed) { component.Seed = seed; @@ -77,6 +105,120 @@ public sealed class BiomeSystem : SharedBiomeSystem Dirty(component); } + public void ClearTemplate(BiomeComponent component) + { + component.Layers.Clear(); + component.Template = null; + Dirty(component); + } + + /// + /// Sets the and refreshes layers. + /// + 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); + } + + /// + /// Adds the specified layer at the specified marker if it exists. + /// + 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(marker)) + { + // TODO: Log when we get a sawmill + return; + } + + component.MarkerLayers.Add(marker); + Dirty(component); + } + + /// + /// Adds the specified template at the specified marker if it exists, withour overriding every layer. + /// + public void AddTemplate(BiomeComponent component, string id, BiomeTemplatePrototype template, 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; + + 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(targetMapUid, out var biome)) + return; + + var targetArea = new Box2(targetMap.Position - 64f, targetMap.Position + 64f); + Preload(targetMapUid, biome, targetArea); + } + + /// + /// Preloads biome for the specified area. + /// + 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(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()); + _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(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(layer); + AddMarkerChunksInRange(biome, worldPos, layerProto); + } } } var loadBiomes = AllEntityQuery(); - 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 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(layer); + var buffer = layerProto.Radius / 2f; + mobChunks ??= new HashSet(); + 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(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 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(component.BiomePrototype); component.ModifiedTiles.TryGetValue(chunk, out var modified); modified ??= new HashSet(); @@ -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 } diff --git a/Content.Server/Procedural/DungeonJob.Generator.cs b/Content.Server/Procedural/DungeonJob.Generator.cs index f9b8f826f5..28f4eb5e4e 100644 --- a/Content.Server/Procedural/DungeonJob.Generator.cs +++ b/Content.Server/Procedural/DungeonJob.Generator.cs @@ -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(); 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; } } diff --git a/Content.Server/Procedural/DungeonJob.PostGen.cs b/Content.Server/Procedural/DungeonJob.PostGen.cs index 9f1e410a60..a5d4d0101f 100644 --- a/Content.Server/Procedural/DungeonJob.PostGen.cs +++ b/Content.Server/Procedural/DungeonJob.PostGen.cs @@ -110,13 +110,11 @@ public sealed partial class DungeonJob var rooms = new List(dungeon.Rooms); var roomTiles = new List(); 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)) { diff --git a/Content.Server/Procedural/DungeonJob.cs b/Content.Server/Procedural/DungeonJob.cs index e294757036..29abd2dc12 100644 --- a/Content.Server/Procedural/DungeonJob.cs +++ b/Content.Server/Procedural/DungeonJob.cs @@ -28,7 +28,7 @@ public sealed partial class DungeonJob : Job 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 MapGridComponent grid, EntityUid gridUid, int seed, - Vector2 position, + Vector2i position, CancellationToken cancellation = default) : base(maxTime, cancellation) { _sawmill = sawmill; diff --git a/Content.Server/Procedural/DungeonSystem.Commands.cs b/Content.Server/Procedural/DungeonSystem.Commands.cs index 3b0058ca9a..d783eb60c6 100644 --- a/Content.Server/Procedural/DungeonSystem.Commands.cs +++ b/Content.Server/Procedural/DungeonSystem.Commands.cs @@ -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(dungeonUid, out var dungeonGrid)) diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index f8976d0043..083c635b78 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -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 index 0000000000..452ac9d7a2 --- /dev/null +++ b/Content.Server/Salvage/Expeditions/SalvageExpeditionComponent.cs @@ -0,0 +1,44 @@ +using Content.Shared.Salvage; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Salvage.Expeditions; + +/// +/// Designates this entity as holding a salvage expedition. +/// +[RegisterComponent] +public sealed class SalvageExpeditionComponent : Component +{ + public SalvageMissionParams MissionParams = default!; + + /// + /// Where the dungeon is located for initial announcement. + /// + [DataField("dungeonLocation")] + public Vector2 DungeonLocation = Vector2.Zero; + + /// + /// When the expeditions ends. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan EndTime; + + /// + /// Station whose mission this is. + /// + [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 index 0000000000..6aee2cc021 --- /dev/null +++ b/Content.Server/Salvage/Expeditions/SalvageMiningExpeditionComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Salvage.Expeditions; + +/// +/// Tracks expedition data for +/// +[RegisterComponent, Access(typeof(SalvageSystem))] +public sealed class SalvageMiningExpeditionComponent : Component +{ + /// + /// Entities that were present on the shuttle and match the loot tax. + /// + [DataField("exemptEntities")] + public List ExemptEntities = new(); +} diff --git a/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs b/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs new file mode 100644 index 0000000000..3abce55e41 --- /dev/null +++ b/Content.Server/Salvage/Expeditions/SalvageStructureComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.Salvage.Expeditions.Structure; + +/// +/// Mission objective for salvage expeditions. +/// +[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 index 0000000000..daa704cb91 --- /dev/null +++ b/Content.Server/Salvage/Expeditions/SalvageStructureExpeditionComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Salvage; + +namespace Content.Server.Salvage.Expeditions.Structure; + +/// +/// Tracks expedition data for +/// +[RegisterComponent, Access(typeof(SalvageSystem), typeof(SpawnSalvageMissionJob))] +public sealed class SalvageStructureExpeditionComponent : Component +{ + [DataField("structures")] + public readonly List Structures = new(); +} diff --git a/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs new file mode 100644 index 0000000000..3239bc77e5 --- /dev/null +++ b/Content.Server/Salvage/SalvageSystem.ExpeditionConsole.cs @@ -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(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(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(station, out var dataComponent)) + { + state = GetState(dataComponent); + } + else + { + state = new SalvageExpeditionConsoleState(TimeSpan.Zero, false, true, 0, new List()); + } + + _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 index 0000000000..9787e3a5d3 --- /dev/null +++ b/Content.Server/Salvage/SalvageSystem.Expeditions.cs @@ -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(OnSalvageExpStationInit); + + SubscribeLocalEvent(OnSalvageConsoleInit); + SubscribeLocalEvent(OnSalvageConsoleParent); + SubscribeLocalEvent(OnSalvageClaimMessage); + + SubscribeLocalEvent(OnDataUnpaused); + + SubscribeLocalEvent(OnExpeditionShutdown); + SubscribeLocalEvent(OnExpeditionUnpaused); + + SubscribeLocalEvent(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(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(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()) + { + // Update offers + if (comp.NextOffer > currentTime || comp.Claimed) + continue; + + comp.Cooldown = false; + comp.NextOffer += MissionCooldown; + GenerateMissions(comp); + UpdateConsoles(comp); + } + + var query = EntityQueryEnumerator(); + + 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(expedition.Owner, out var mining)) + { + var xformQuery = GetEntityQuery(); + var entities = new List(); + 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); + } + + /// + /// Deducts ore tax for mining. + /// + private void MiningTax(List entities, EntityUid entity, SalvageMiningExpeditionComponent mining, EntityQuery 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().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 index 0000000000..9cb56aeee9 --- /dev/null +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -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(OnFTLRequest); + SubscribeLocalEvent(OnFTLStarted); + SubscribeLocalEvent(OnFTLCompleted); + } + + /// + /// Announces status updates to salvage crewmembers on the state of the expedition. + /// + private void Announce(EntityUid mapUid, string text) + { + var mapId = Comp(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(ev.MapUid) || + !TryComp(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(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( + _mapManager.GetMapEntityId(ev.TargetCoordinates.ToMap(EntityManager, _transform).MapId), + out var mining)) + { + var ents = new List(); + var xformQuery = GetEntityQuery(); + MiningTax(ents, ev.Entity, mining, xformQuery); + mining.ExemptEntities = ents; + } + + if (!TryComp(ev.FromMapUid, out var expedition) || + !TryComp(expedition.Station, out var station)) + { + return; + } + + // Check if any shuttles remain. + var query = EntityQueryEnumerator(); + + 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(); + + // 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(); + + if (TryComp(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(shuttleUid)) + continue; + + _shuttle.FTLTravel(shuttleUid, shuttle, member, ftlTime); + } + + break; + } + } + } + } + + // Mining missions: NOOP + + // Structure missions + var structureQuery = EntityQueryEnumerator(); + + 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))); + } + } + } +} diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index 8fe166f3c7..3a3e2190b9 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -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(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 index 0000000000..b7de8fac27 --- /dev/null +++ b/Content.Server/Salvage/SpawnSalvageMissionJob.cs @@ -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 +{ + 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 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(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() + .GetMission(_missionParams.MissionType, _missionParams.Difficulty, _missionParams.Seed); + + var missionBiome = _prototypeManager.Index(mission.Biome); + + if (missionBiome.BiomePrototype != null) + { + var biome = _entManager.AddComponent(mapUid); + var biomeSystem = _entManager.System(); + biomeSystem.SetTemplate(biome, _prototypeManager.Index(missionBiome.BiomePrototype)); + biomeSystem.SetSeed(biome, mission.Seed); + _entManager.Dirty(biome); + + // Gravity + var gravity = _entManager.EnsureComponent(mapUid); + gravity.Enabled = true; + _entManager.Dirty(gravity, metadata); + + // Atmos + var atmos = _entManager.EnsureComponent(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(mapUid); + lighting.AmbientLightColor = mission.Color.Value; + _entManager.Dirty(lighting); + } + } + + _mapManager.DoMapInitialize(mapId); + _mapManager.SetMapPaused(mapId, true); + + // Setup expedition + var expedition = _entManager.AddComponent(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(ftlUid).EntityName = SharedSalvageSystem.GetFTLName(_prototypeManager.Index("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(mission.Dungeon); + var dungeonConfig = _prototypeManager.Index(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 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(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 reservedTiles) + { + for (var i = 0; i < loot.LootRules.Count; i++) + { + var rule = loot.LootRules[i]; + + switch (rule) + { + case BiomeTemplateLoot biomeLoot: + if (_entManager.TryGetComponent(gridUid, out var biome)) + { + _biome.AddTemplate(biome, "Loot", _prototypeManager.Index(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 reservedTiles) + { + var spawnTiles = new HashSet(); + + 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(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(mission.Faction); + + if (_entManager.TryGetComponent(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(gridUid); + var availableRooms = dungeon.Rooms.ToList(); + var faction = _prototypeManager.Index(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(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 +} diff --git a/Content.Server/Shuttles/Events/FTLCompletedEvent.cs b/Content.Server/Shuttles/Events/FTLCompletedEvent.cs index 6eb228fbd8..92ba6ee31d 100644 --- a/Content.Server/Shuttles/Events/FTLCompletedEvent.cs +++ b/Content.Server/Shuttles/Events/FTLCompletedEvent.cs @@ -6,4 +6,4 @@ namespace Content.Server.Shuttles.Events; /// Raised when has completed FTL Travel. /// [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 index 0000000000..7105407157 --- /dev/null +++ b/Content.Server/Shuttles/Events/FTLRequestEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Shuttles.Events; + +/// +/// Raised by a shuttle when it has requested an FTL. +/// +[ByRefEvent] +public record struct FTLRequestEvent(EntityUid MapUid); diff --git a/Content.Server/Shuttles/Events/FTLStartedEvent.cs b/Content.Server/Shuttles/Events/FTLStartedEvent.cs index 091559163f..965da7f0c5 100644 --- a/Content.Server/Shuttles/Events/FTLStartedEvent.cs +++ b/Content.Server/Shuttles/Events/FTLStartedEvent.cs @@ -6,4 +6,4 @@ namespace Content.Server.Shuttles.Events; /// Raised when a shuttle has moved to FTL space. /// [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); diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index 9c6de9106f..a9b3be6393 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -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); } /// @@ -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(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(targetXform.GridUid)) diff --git a/Content.Shared/Parallax/Biomes/BiomeComponent.cs b/Content.Shared/Parallax/Biomes/BiomeComponent.cs index 9b8d8ca430..c4ef905835 100644 --- a/Content.Shared/Parallax/Biomes/BiomeComponent.cs +++ b/Content.Shared/Parallax/Biomes/BiomeComponent.cs @@ -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))] + /// + /// The underlying entity, decal, and tile layers for the biome. + /// + [DataField("layers")] [AutoNetworkedField] - public string BiomePrototype = "Grasslands"; + public List 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. + /// + /// Templates to use for . Optional as this can be set elsewhere. + /// + /// + /// This is really just here for prototype reload support. + /// + [ViewVariables(VVAccess.ReadWrite), + DataField("template", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? Template; /// /// 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 LoadedChunks = new(); + #region Markers + /// - /// 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. /// - public bool Generating = false; + [DataField("loadedMarkers", customTypeSerializer:typeof(PrototypeIdDictionarySerializer, BiomeMarkerLayerPrototype>))] + public readonly Dictionary> LoadedMarkers = new(); + + [DataField("markerLayers", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List MarkerLayers = new(); + + #endregion } diff --git a/Content.Shared/Parallax/Biomes/BiomePrototype.cs b/Content.Shared/Parallax/Biomes/BiomePrototype.cs deleted file mode 100644 index 430c8a1bda..0000000000 --- a/Content.Shared/Parallax/Biomes/BiomePrototype.cs +++ /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 Layers = new(); -} - -[ImplicitDataDefinitionForInheritors] -public interface IBiomeLayer -{ - /// - /// Seed is used an offset from the relevant BiomeComponent's seed. - /// - FastNoiseLite Noise { get; } - - /// - /// Threshold for this layer to be present. If set to 0 forces it for every tile. - /// - float Threshold { get; } -} - -public sealed class BiomeTileLayer : IBiomeLayer -{ - [DataField("noise")] public FastNoiseLite Noise { get; } = new(0); - - /// - [DataField("threshold")] - public float Threshold { get; } = 0.5f; - - /// - /// Which tile variants to use for this layer. Uses all of the tile's variants if none specified - /// - [DataField("variants")] - public List? Variants = null; - - [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Tile = string.Empty; -} - -/// -/// Handles actual objects such as decals and entities. -/// -public interface IBiomeWorldLayer : IBiomeLayer -{ - /// - /// What tiles we're allowed to spawn on, real or biome. - /// - List AllowedTiles { get; } -} - -public sealed class BiomeDecalLayer : IBiomeWorldLayer -{ - /// - [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List AllowedTiles { get; } = new(); - - /// - /// Divide each tile up by this amount. - /// - [DataField("divisions")] - public float Divisions = 1f; - - [DataField("noise")] - public FastNoiseLite Noise { get; } = new(0); - - /// - [DataField("threshold")] - public float Threshold { get; } = 0.8f; - - [DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Decals = new(); -} - -public sealed class BiomeEntityLayer : IBiomeWorldLayer -{ - /// - [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List AllowedTiles { get; } = new(); - - [DataField("noise")] public FastNoiseLite Noise { get; } = new(0); - - /// - [DataField("threshold")] - public float Threshold { get; } = 0.5f; - - [DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Entities = new(); -} diff --git a/Content.Shared/Parallax/Biomes/BiomeTemplatePrototype.cs b/Content.Shared/Parallax/Biomes/BiomeTemplatePrototype.cs new file mode 100644 index 0000000000..74a98f67f0 --- /dev/null +++ b/Content.Shared/Parallax/Biomes/BiomeTemplatePrototype.cs @@ -0,0 +1,17 @@ +using Content.Shared.Parallax.Biomes.Layers; +using Robust.Shared.Noise; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Parallax.Biomes; + +/// +/// A preset group of biome layers to be used for a +/// +[Prototype("biomeTemplate")] +public sealed class BiomeTemplatePrototype : IPrototype +{ + [IdDataField] public string ID { get; } = default!; + + [DataField("layers")] + public List Layers = new(); +} diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeDecalLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeDecalLayer.cs new file mode 100644 index 0000000000..07e674c802 --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Layers/BiomeDecalLayer.cs @@ -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 +{ + /// + [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer))] + public List AllowedTiles { get; } = new(); + + /// + /// Divide each tile up by this amount. + /// + [DataField("divisions")] + public float Divisions = 1f; + + [DataField("noise")] + public FastNoiseLite Noise { get; } = new(0); + + /// + [DataField("threshold")] + public float Threshold { get; } = 0.8f; + + /// + [DataField("invert")] public bool Invert { get; } = false; + + [DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer))] + public List Decals = new(); +} diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeDummyLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeDummyLayer.cs new file mode 100644 index 0000000000..5395ebf754 --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Layers/BiomeDummyLayer.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Noise; +using Robust.Shared.Serialization; + +namespace Content.Shared.Parallax.Biomes.Layers; + +/// +/// 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. +/// +[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 index 0000000000..caf2fef991 --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Layers/BiomeEntityLayer.cs @@ -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 +{ + /// + [DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer))] + public List AllowedTiles { get; } = new(); + + [DataField("noise")] public FastNoiseLite Noise { get; } = new(0); + + /// + [DataField("threshold")] + public float Threshold { get; } = 0.5f; + + /// + [DataField("invert")] public bool Invert { get; } = false; + + [DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List Entities = new(); +} diff --git a/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs new file mode 100644 index 0000000000..7b8aa815a4 --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Layers/BiomeTileLayer.cs @@ -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); + + /// + [DataField("threshold")] + public float Threshold { get; } = 0.5f; + + /// + [DataField("invert")] public bool Invert { get; } = false; + + /// + /// Which tile variants to use for this layer. Uses all of the tile's variants if none specified + /// + [DataField("variants")] + public List? Variants = null; + + [DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + 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 index 0000000000..3f81b5598f --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Layers/IBiomeLayer.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Noise; + +namespace Content.Shared.Parallax.Biomes.Layers; + +[ImplicitDataDefinitionForInheritors] +public interface IBiomeLayer +{ + /// + /// Seed is used an offset from the relevant BiomeComponent's seed. + /// + FastNoiseLite Noise { get; } + + /// + /// Threshold for this layer to be present. If set to 0 forces it for every tile. + /// + float Threshold { get; } + + /// + /// Is the thresold inverted so we need to be lower than it. + /// + 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 index 0000000000..92632e24ed --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Layers/IBiomeWorldLayer.cs @@ -0,0 +1,12 @@ +namespace Content.Shared.Parallax.Biomes.Layers; + +/// +/// Handles actual objects such as decals and entities. +/// +public interface IBiomeWorldLayer : IBiomeLayer +{ + /// + /// What tiles we're allowed to spawn on, real or biome. + /// + List AllowedTiles { get; } +} diff --git a/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs b/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs new file mode 100644 index 0000000000..e70f0541be --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Markers/BiomeMarkerLayerPrototype.cs @@ -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))] + public string Prototype = string.Empty; + + /// + [DataField("radius")] + public float Radius { get; } = 12f; + + /// + /// How many mobs to spawn in one group. + /// + [DataField("groupCount")] + public int GroupCount = 1; + + /// + [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 index 0000000000..7c47cf9a2f --- /dev/null +++ b/Content.Shared/Parallax/Biomes/Markers/IBiomeMarkerLayer.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Parallax.Biomes.Points; + +/// +/// 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. +/// +public interface IBiomeMarkerLayer : IPrototype +{ + /// + /// Minimum radius between 2 points + /// + [DataField("radius")] + public float Radius { get; } + + /// + /// How large the pre-generated points area is. + /// + [DataField("size")] + public int Size { get; } +} diff --git a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs index 1e34e63cda..9d08f9682e 100644 --- a/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs +++ b/Content.Shared/Parallax/Biomes/SharedBiomeSystem.cs @@ -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(biome.BiomePrototype), - biome.Noise, grid, out tile); + return TryGetBiomeTile(indices, biome.Layers, biome.Noise, grid, out tile); } /// /// Tries to get the tile, real or otherwise, for the specified indices. /// - public bool TryGetBiomeTile(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile) + public bool TryGetBiomeTile(Vector2i indices, List 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(tileLayer.Tile), tileLayer.Variants, out tile)) + if (TryGetTile(indices, noise, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index(tileLayer.Tile), tileLayer.Variants, out tile)) { noise.SetSeed(oldSeed); return true; @@ -127,9 +125,10 @@ public abstract class SharedBiomeSystem : EntitySystem /// /// Gets the underlying biome tile, ignoring any existing tile that may be there. /// - private bool TryGetTile(Vector2i indices, FastNoiseLite seed, float threshold, ContentTileDefinition tileDef, List? variants, [NotNullWhen(true)] out Tile? tile) + private bool TryGetTile(Vector2i indices, FastNoiseLite seed, bool invert, float threshold, ContentTileDefinition tileDef, List? 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 /// /// Tries to get the relevant entity for this tile. /// - protected bool TryGetEntity(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid, + protected bool TryGetEntity(Vector2i indices, List 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 /// /// Tries to get the relevant decals for this tile. /// - public bool TryGetDecals(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid, + public bool TryGetDecals(Vector2i indices, List 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; diff --git a/Content.Shared/Procedural/Dungeon.cs b/Content.Shared/Procedural/Dungeon.cs index a1c51f6e8b..6aef0c1bdf 100644 --- a/Content.Shared/Procedural/Dungeon.cs +++ b/Content.Shared/Procedural/Dungeon.cs @@ -2,6 +2,13 @@ namespace Content.Shared.Procedural; public sealed class Dungeon { + /// + /// Starting position used to generate the dungeon from. + /// + public Vector2i Position; + + public Vector2i Center; + public List Rooms = new(); /// diff --git a/Content.Shared/Procedural/Loot/BiomeTemplateLoot.cs b/Content.Shared/Procedural/Loot/BiomeTemplateLoot.cs new file mode 100644 index 0000000000..b4972e525d --- /dev/null +++ b/Content.Shared/Procedural/Loot/BiomeTemplateLoot.cs @@ -0,0 +1,13 @@ +using Content.Shared.Parallax.Biomes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Procedural.Loot; + +/// +/// Adds a biome template layer for dungeon loot. +/// +public sealed class BiomeTemplateLoot : IDungeonLoot +{ + [DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + 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 index ab11f337bb..0000000000 --- a/Content.Shared/Procedural/Loot/ClusterLoot.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Procedural.Loot; - -/// -/// Spawns loot at points in the specified rooms -/// -public sealed class ClusterLoot : IDungeonLoot -{ - /// - /// Minimum spawns in a cluster. - /// - [DataField("minCluster")] - public int MinClusterAmount; - - /// - /// Maximum spawns in a cluster. - /// - [DataField("maxCluster")] public int MaxClusterAmount; - - /// - /// Amount to spawn for the entire loot. - /// - [DataField("max")] - public int Amount; - - /// - /// Number of points to spawn. - /// - [DataField("points")] public int Points; - - [DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - 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 index 0000000000..f1f46079c2 --- /dev/null +++ b/Content.Shared/Procedural/Loot/DungeonClusterLoot.cs @@ -0,0 +1,24 @@ +using Content.Shared.Random; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Procedural.Loot; + +/// +/// Spawns loot at points in the specified area inside of a dungeon room. +/// +public sealed class DungeonClusterLoot : IDungeonLoot +{ + /// + /// Spawns in a cluster. + /// + [DataField("clusterAmount")] + public int ClusterAmount = 1; + + /// + /// Number of clusters to spawn. + /// + [DataField("clusters")] public int Points = 1; + + [DataField("lootTable", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Prototype { get; } = string.Empty; +} diff --git a/Content.Shared/Procedural/Loot/IDungeonLoot.cs b/Content.Shared/Procedural/Loot/IDungeonLoot.cs index 933e5e5929..6b9cd32cd2 100644 --- a/Content.Shared/Procedural/Loot/IDungeonLoot.cs +++ b/Content.Shared/Procedural/Loot/IDungeonLoot.cs @@ -3,5 +3,4 @@ namespace Content.Shared.Procedural.Loot; [ImplicitDataDefinitionForInheritors] public interface IDungeonLoot { - string Prototype { get; } } diff --git a/Content.Shared/Procedural/Loot/SalvageLootPrototype.cs b/Content.Shared/Procedural/Loot/SalvageLootPrototype.cs index 642dd8c003..c485e15cf3 100644 --- a/Content.Shared/Procedural/Loot/SalvageLootPrototype.cs +++ b/Content.Shared/Procedural/Loot/SalvageLootPrototype.cs @@ -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; + /// + /// Mission types this loot is not allowed to spawn for + /// + [DataField("blacklist")] + public List Blacklist = new(); + /// /// All of the loot rules /// diff --git a/Content.Shared/Procedural/Rewards/BankReward.cs b/Content.Shared/Procedural/Rewards/BankReward.cs deleted file mode 100644 index 08218eadf3..0000000000 --- a/Content.Shared/Procedural/Rewards/BankReward.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Shared.Procedural.Rewards; - -/// -/// Payout to the station's bank account. -/// -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 index da18b2fe17..0000000000 --- a/Content.Shared/Procedural/Rewards/ISalvageReward.cs +++ /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 index c8fd126d64..0000000000 --- a/Content.Shared/Procedural/Rewards/SalvageRewardPrototype.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Robust.Shared.Prototypes; - -namespace Content.Shared.Procedural.Rewards; - -/// -/// Given after successful completion of a salvage mission. -/// -[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 index e637fe125d..0000000000 --- a/Content.Shared/Salvage/Expeditions/Extraction/SalvageExtraction.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Content.Shared.Salvage.Expeditions.Extraction; - -public sealed class SalvageExtraction : ISalvageMission -{ - /// - /// Minimum weight to be used for a wave. - /// - [DataField("minWaveWeight")] public float MinWaveWeight = 5; - - /// - /// Minimum time between 2 waves. Roughly the end of one to the start of another. - /// - [ViewVariables(VVAccess.ReadWrite), DataField("waveCooldown")] - public TimeSpan WaveCooldown = TimeSpan.FromSeconds(60); - - /// - /// How much weight accumulates per second while the expedition is active. - /// - [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 index cd1186c3c8..0000000000 --- a/Content.Shared/Salvage/Expeditions/IFactionExpeditionConfig.cs +++ /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 index 0000000000..d911e4ee42 --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/ISalvageMod.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Salvage.Expeditions.Modifiers; + +public interface ISalvageMod +{ + /// + /// Player-friendly version describing this modifier. + /// + 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 index 0000000000..a5887b0c9f --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageBiomeMod.cs @@ -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; + +/// +/// Affects the biome to be used for salvage. +/// +[Prototype("salvageBiomeMod")] +public sealed class SalvageBiomeMod : IPrototype, ISalvageMod +{ + [IdDataField] public string ID { get; } = default!; + + [DataField("desc")] public string Description { get; } = string.Empty; + + /// + /// Cost for difficulty modifiers. + /// + [DataField("cost")] + public float Cost { get; } = 0f; + + /// + /// Is weather allowed to apply to this biome. + /// + [DataField("weather")] + public bool Weather = true; + + [DataField("biome", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + 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 index 0000000000..8a7ce1ccca --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs @@ -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))] + public string Proto = string.Empty; + + /// + /// Cost for difficulty modifiers. + /// + [DataField("cost")] + public float Cost { get; } = 0f; + + /// + /// Biomes this dungeon can occur in. + /// + [DataField("biomeMods", customTypeSerializer:typeof(PrototypeIdListSerializer))] + public List? BiomeMods; +} diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageLightMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageLightMod.cs new file mode 100644 index 0000000000..7393bf862f --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageLightMod.cs @@ -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; + + /// + /// Cost for difficulty modifiers. + /// + [DataField("cost")] + public float Cost { get; } = 0f; + + [DataField("color", required: true)] public Color? Color; + + /// + /// Biomes that this color applies to. + /// + [DataField("biomes", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List? Biomes; +} diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageMod.cs new file mode 100644 index 0000000000..d353fac571 --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageMod.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Salvage.Expeditions.Modifiers; + +/// +/// Generic modifiers with no additional data +/// +[Prototype("salvageMod")] +public sealed class SalvageMod : IPrototype, ISalvageMod +{ + [IdDataField] public string ID { get; } = default!; + + [DataField("desc")] public string Description { get; } = string.Empty; + + /// + /// Cost for difficulty modifiers. + /// + [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 index 0000000000..4f1ab80b7c --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageTimeMod.cs @@ -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; + + /// + /// Cost for difficulty modifiers. + /// + [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 index 0000000000..7889e8b09b --- /dev/null +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageWeatherMod.cs @@ -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; + + /// + /// Cost for difficulty modifiers. + /// + [DataField("cost")] + public float Cost { get; } = 0f; + + [DataField("weather", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))] + public string WeatherPrototype = string.Empty; + + /// + /// Whitelist for biomes. If empty assumed any allowed. + /// + [DataField("biomes", customTypeSerializer:typeof(PrototypeIdListSerializer))] + public List Biomes = new(); +} diff --git a/Content.Shared/Salvage/Expeditions/SalvageFactionPrototype.cs b/Content.Shared/Salvage/Expeditions/SalvageFactionPrototype.cs index 03184aca70..5852838285 100644 --- a/Content.Shared/Salvage/Expeditions/SalvageFactionPrototype.cs +++ b/Content.Shared/Salvage/Expeditions/SalvageFactionPrototype.cs @@ -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; + + /// + /// Cost for difficulty modifiers. + /// + [DataField("cost")] + public float Cost { get; } = 0f; + [ViewVariables(VVAccess.ReadWrite), DataField("groups", required: true)] public List MobGroups = default!; /// - /// Per expedition type data for this faction. + /// Miscellaneous data for factions. /// - [ViewVariables(VVAccess.ReadWrite), DataField("configs", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] - public Dictionary Configs = new(); + [ViewVariables(VVAccess.ReadWrite), DataField("configs")] + public Dictionary Configs = new(); } diff --git a/Content.Shared/Salvage/Expeditions/Structure/SalvageStructure.cs b/Content.Shared/Salvage/Expeditions/Structure/SalvageStructure.cs deleted file mode 100644 index 052c6f6a81..0000000000 --- a/Content.Shared/Salvage/Expeditions/Structure/SalvageStructure.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Content.Shared.Salvage.Expeditions.Structure; - -/// -/// Destroy the specified number of structures to finish the expedition. -/// -[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 index 3193a1d83b..0000000000 --- a/Content.Shared/Salvage/Expeditions/Structure/SalvageStructureFaction.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Salvage.Expeditions.Structure; - -/// -/// Per-faction config for Salvage Structure expeditions. -/// -[DataDefinition] -public sealed class SalvageStructureFaction : IFactionExpeditionConfig -{ - /// - /// Entity prototype of the structures to destroy. - /// - [ViewVariables(VVAccess.ReadWrite), DataField("spawn", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Spawn = default!; - - /// - /// How many groups of mobs to spawn. - /// - [DataField("groupCount")] - public int Groups = 5; -} diff --git a/Content.Shared/Salvage/ISalvageMission.cs b/Content.Shared/Salvage/ISalvageMission.cs deleted file mode 100644 index b1f08b45f2..0000000000 --- a/Content.Shared/Salvage/ISalvageMission.cs +++ /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 index eec3cc8d1a..0000000000 --- a/Content.Shared/Salvage/SalvageExpeditionPrototype.cs +++ /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!; - - /// - /// Naming scheme for the FTL marker. - /// - [DataField("nameProto", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string NameProto = "names_borer"; - - /// - /// Biome to generate the dungeon. - /// - [DataField("biome", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Biome = string.Empty; - - /// - /// Player-friendly description for the console. - /// - [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); - - /// - /// Available factions for selection for this mission prototype. - /// - [DataField("factions", customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Factions = new(); - - [DataField("dungeonConfig", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DungeonConfigPrototype = string.Empty; - - [DataField("reward", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string Reward = string.Empty; - - /// - /// Possible loot prototypes available for this expedition. - /// This spawns during the mission and is not tied to completion. - /// - [DataField("loot", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List Loots = new(); - - [DataField("dungeonPosition")] - public Vector2i DungeonPosition = new(80, -25); -} - -[Serializable, NetSerializable] -public enum DifficultyRating : byte -{ - None, - Minor, - Moderate, - Hazardous, - Extreme, -} - diff --git a/Content.Shared/Salvage/SalvageExpeditions.cs b/Content.Shared/Salvage/SalvageExpeditions.cs index 680a71090c..18247470ff 100644 --- a/Content.Shared/Salvage/SalvageExpeditions.cs +++ b/Content.Shared/Salvage/SalvageExpeditions.cs @@ -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 Missions; + public List Missions; - public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, ushort activeMission, List missions) + public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, bool cooldown, ushort activeMission, List 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; + /// + /// Are we actively cooling down from the last salvage mission. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("cooldown")] + public bool Cooldown = false; + /// /// Nexy time salvage missions are offered. /// @@ -56,7 +65,7 @@ public sealed class SalvageExpeditionDataComponent : Component public TimeSpan NextOffer; [ViewVariables] - public readonly Dictionary Missions = new(); + public readonly Dictionary 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; + /// + /// Base difficulty for this mission. + /// + [ViewVariables(VVAccess.ReadWrite)] public DifficultyRating Difficulty; } -[Serializable, NetSerializable] -public enum SalvageEnvironment : byte +/// +/// Created from . Only needed for data the client also needs for mission +/// display. +/// +public sealed record SalvageMission( + int Seed, + DifficultyRating Difficulty, + string Dungeon, + string Faction, + SalvageMissionType Mission, + string Biome, + Color? Color, + TimeSpan Duration, + Dictionary Loot, + List Modifiers) { - Invalid = 0, - Caves, + /// + /// Seed used for the mission. + /// + public readonly int Seed = Seed; + + /// + /// Difficulty rating. + /// + public DifficultyRating Difficulty = Difficulty; + + /// + /// to be used. + /// + public readonly string Dungeon = Dungeon; + + /// + /// to be used. + /// + public readonly string Faction = Faction; + + /// + /// Underlying mission params that generated this. + /// + public readonly SalvageMissionType Mission = Mission; + + /// + /// Biome to be used for the mission. + /// + public readonly string Biome = Biome; + + /// + /// Lighting color to be used (AKA outdoor lighting). + /// + public readonly Color? Color = Color; + + /// + /// Mission duration. + /// + public TimeSpan Duration = Duration; + + public Dictionary Loot = Loot; + + /// + /// Modifiers (outside of the above) applied to the mission. + /// + public List Modifiers = Modifiers; } [Serializable, NetSerializable] diff --git a/Content.Shared/Salvage/SharedSalvageSystem.cs b/Content.Shared/Salvage/SharedSalvageSystem.cs index b66dd1375b..06e5d1faba 100644 --- a/Content.Shared/Salvage/SharedSalvageSystem.cs +++ b/Content.Shared/Salvage/SharedSalvageSystem.cs @@ -1,77 +1,235 @@ +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 { - public static readonly TimeSpan MissionCooldown = TimeSpan.FromMinutes(5); - public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(10); + [Dependency] private readonly ILocalizationManager _loc = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; - public static float GetDifficultyModifier(DifficultyRating difficulty) + public static readonly TimeSpan MissionCooldown = TimeSpan.FromMinutes(5); + public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(15); + + #region Descriptions + + 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(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; + } + + /// + /// Gets the amount of structures to destroy. + /// + 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); } } + /// + /// How many groups of mobs to spawn for a mission. + /// + 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 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(rand, ref rating); + var biome = GetMod(rand, ref rating); + var dungeon = GetDungeon(biome.ID, rand, ref rating); + var mods = new List(); + + SalvageLightMod? light = null; + + if (biome.BiomePrototype != null) + { + light = GetLight(biome.ID, rand, ref rating); + mods.Add(light.Description); + } + + var time = GetMod(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().ToList(), GetDifficulty(difficulty), seed); + return new SalvageMission(seed, difficulty, dungeon.ID, faction.ID, config, biome.ID, light?.Color, duration, loots, mods); } - public static IEnumerable GetLoot(List loots, int seed, IPrototypeManager protoManager) + public SalvageDungeonMod GetDungeon(string biome, System.Random rand, ref float rating) { + var mods = _proto.EnumeratePrototypes().ToList(); + mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal)); + rand.Shuffle(mods); + + foreach (var mod in mods) + { + if (mod.BiomeMods?.Contains(biome) == false || + mod.Cost > rating) + { + continue; + } + + rating -= (int) mod.Cost; + + return mod; + } + + throw new InvalidOperationException(); + } + + public SalvageLightMod GetLight(string biome, System.Random rand, ref float rating) + { + var mods = _proto.EnumeratePrototypes().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(); + } + + public T GetMod(System.Random rand, ref float rating) where T : class, IPrototype, ISalvageMod + { + var mods = _proto.EnumeratePrototypes().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(); + } + + private Dictionary GetLoot(SalvageMissionType mission, List loots, int count, int seed) + { + var results = new Dictionary(); var adjustedSeed = new System.Random(seed + 2); - for (var i = 0; i < loots.Count; i++) + for (var i = 0; i < count; i++) { - var loot = loots[i]; - var a = protoManager.Index(loot); - var lootConfig = a.Pick(adjustedSeed); - yield return protoManager.Index(lootConfig); + 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; } - - public static ISalvageReward GetReward(WeightedRandomPrototype proto, int seed, IPrototypeManager protoManager) - { - var adjustedSeed = new System.Random(seed + 3); - var rewardProto = proto.Pick(adjustedSeed); - return protoManager.Index(rewardProto).Reward; - } - - #region Structure - - public static int GetStructureCount(SalvageStructure structure, int seed) - { - var adjustedSeed = new System.Random(seed + 4); - return adjustedSeed.Next(structure.MinStructures, structure.MaxStructures + 1); - } - - #endregion +} + +[Serializable, NetSerializable] +public enum SalvageMissionType : byte +{ + /// + /// No dungeon, just ore loot and random mob spawns. + /// + Mining, + + /// + /// Destroy the specified structures in a dungeon. + /// + 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 index 0000000000..d24ec7d72e --- /dev/null +++ b/Resources/Locale/en-US/procedural/biome.ftl @@ -0,0 +1,6 @@ +cmd-biome_clear-desc = Clears a biome entirely +cmd-biome_clear-help = biome_clear +cmd-biome_addlayer-desc = Adds another biome layer +cmd-biome_addlayer-help = biome_addlayer [seed offset] +cmd-biome_addmarkerlayer-desc = Adds another biome marker layer +cmd-biome_addmarkerlayer-help = biome_addmarkerlayer diff --git a/Resources/Locale/en-US/procedural/expeditions.ftl b/Resources/Locale/en-US/procedural/expeditions.ftl new file mode 100644 index 0000000000..126f284176 --- /dev/null +++ b/Resources/Locale/en-US/procedural/expeditions.ftl @@ -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}. diff --git a/Resources/Maps/Salvage/medium-1.yml b/Resources/Maps/Salvage/medium-1.yml index c5600b52a9..3799dc219f 100644 --- a/Resources/Maps/Salvage/medium-1.yml +++ b/Resources/Maps/Salvage/medium-1.yml @@ -1089,12 +1089,6 @@ entities: - pos: 0.5,0.5 parent: 0 type: Transform -- uid: 85 - type: PaperWrittenSalvageLoreMedium1PlasmaTrap - components: - - pos: 0.48327154,0.5698495 - parent: 0 - type: Transform - uid: 86 type: ClothingEyesGlassesMeson components: diff --git a/Resources/Maps/Salvage/small-2.yml b/Resources/Maps/Salvage/small-2.yml index 1aa25af3ef..2635fc6341 100644 --- a/Resources/Maps/Salvage/small-2.yml +++ b/Resources/Maps/Salvage/small-2.yml @@ -568,12 +568,6 @@ entities: - pos: -3.5,0.5 parent: 0 type: Transform -- uid: 55 - type: SalvageLorePaperGamingSpawner - components: - - pos: -1.5,-2.5 - parent: 0 - type: Transform - uid: 56 type: SalvageMobSpawner75 components: diff --git a/Resources/Prototypes/Catalog/Fills/Paper/salvage_lore.yml b/Resources/Prototypes/Catalog/Fills/Paper/salvage_lore.yml deleted file mode 100644 index 22494b05f9..0000000000 --- a/Resources/Prototypes/Catalog/Fills/Paper/salvage_lore.yml +++ /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 -# ---- diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index bfe7cfc64e..fde5e6d4fe 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -89,6 +89,17 @@ - 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 diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 8bbf94c430..0fd8d2c811 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -717,6 +717,49 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Specific/xeno.yml b/Resources/Prototypes/Entities/Structures/Specific/xeno.yml index a1c5e64779..d6f624a898 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/xeno.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/xeno.yml @@ -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 index 0000000000..2f386f676b --- /dev/null +++ b/Resources/Prototypes/Procedural/biome_markers.yml @@ -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 index 0000000000..536b97e997 --- /dev/null +++ b/Resources/Prototypes/Procedural/biome_ore_templates.yml @@ -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 diff --git a/Resources/Prototypes/biomes.yml b/Resources/Prototypes/Procedural/biome_templates.yml similarity index 91% rename from Resources/Prototypes/biomes.yml rename to Resources/Prototypes/Procedural/biome_templates.yml index 625cad2d95..f6144977e4 100644 --- a/Resources/Prototypes/biomes.yml +++ b/Resources/Prototypes/Procedural/biome_templates.yml @@ -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 @@ -142,6 +142,8 @@ cellularReturnType: Distance2 entities: - WallRock + - !type:BiomeDummyLayer + id: Loot # Water - !type:BiomeEntityLayer allowedTiles: @@ -195,9 +197,8 @@ cellularReturnType: Distance2 # Lava -- type: biome +- type: biomeTemplate id: Lava - desc: Lava layers: - !type:BiomeEntityLayer threshold: 0.9 @@ -255,6 +256,8 @@ - FloorBasalt entities: - FloorLavaEntity + - !type:BiomeDummyLayer + id: Loot # Fill basalt - !type:BiomeTileLayer threshold: -1 @@ -263,7 +266,7 @@ tile: FloorBasalt # Snow -- type: biome +- type: biomeTemplate id: Snow # Similar to Grasslands... but snow layers: # Sparse vegetation @@ -353,6 +356,8 @@ - FloraTreeSnow04 - FloraTreeSnow05 - FloraTreeSnow06 + - !type:BiomeDummyLayer + id: Loot - !type:BiomeTileLayer threshold: -1.0 tile: FloorSnow @@ -363,3 +368,27 @@ 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 diff --git a/Resources/Prototypes/Procedural/dungeon_room_packs.yml b/Resources/Prototypes/Procedural/dungeon_room_packs.yml index 63766a0693..28bf1481cd 100644 --- a/Resources/Prototypes/Procedural/dungeon_room_packs.yml +++ b/Resources/Prototypes/Procedural/dungeon_room_packs.yml @@ -129,4 +129,4 @@ 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 index 0000000000..06e088ac86 --- /dev/null +++ b/Resources/Prototypes/Procedural/salvage_factions.yml @@ -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 index 0000000000..a14722eeea --- /dev/null +++ b/Resources/Prototypes/Procedural/salvage_loot.yml @@ -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 index 0000000000..e69de29bb2 diff --git a/Resources/Prototypes/Procedural/salvage_mods.yml b/Resources/Prototypes/Procedural/salvage_mods.yml new file mode 100644 index 0000000000..d9e36a998d --- /dev/null +++ b/Resources/Prototypes/Procedural/salvage_mods.yml @@ -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