Salvage expeditions (#12745)

This commit is contained in:
metalgearsloth
2023-04-20 10:43:13 +10:00
committed by GitHub
parent 486d7c179e
commit 122350f19c
79 changed files with 2764 additions and 662 deletions

View File

@@ -128,7 +128,7 @@ namespace Content.Client.Lobby.UI
_viewBox.AddChild(viewWest); _viewBox.AddChild(viewWest);
_viewBox.AddChild(viewEast); _viewBox.AddChild(viewEast);
_summaryLabel.Text = selectedCharacter.Summary; _summaryLabel.Text = selectedCharacter.Summary;
EntitySystem.Get<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter); _entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
GiveDummyJobClothes(_previewDummy.Value, selectedCharacter); GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
} }
} }

View File

@@ -1,11 +1,11 @@
<controls:FancyWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'Salvage expeditions'}" Title="{Loc 'salvage-expedition-window-title'}"
MinSize="800 360"> MinSize="800 360">
<BoxContainer Orientation="Vertical"> <BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal"> <BoxContainer Orientation="Horizontal">
<Label Name="NextOfferLabel" <Label Name="NextOfferLabel"
Text="Next offer:" Text="{Loc 'salvage-expedition-window-next'}"
Margin="5"></Label> Margin="5"></Label>
<ProgressBar Name="NextOfferBar" <ProgressBar Name="NextOfferBar"
HorizontalExpand="True" HorizontalExpand="True"

View File

@@ -1,15 +1,11 @@
using System.Linq;
using Content.Client.Computer; using Content.Client.Computer;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Parallax.Biomes; using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot; using Content.Shared.Procedural.Loot;
using Content.Shared.Procedural.Rewards;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Salvage; using Content.Shared.Salvage;
using Content.Shared.Salvage.Expeditions; using Content.Shared.Salvage.Expeditions.Modifiers;
using Content.Shared.Salvage.Expeditions.Structure;
using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.BUIStates;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
@@ -31,6 +27,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
public event Action<ushort>? ClaimMission; public event Action<ushort>? ClaimMission;
private bool _claimed; private bool _claimed;
private bool _cooldown;
private TimeSpan _nextOffer; private TimeSpan _nextOffer;
public SalvageExpeditionWindow() public SalvageExpeditionWindow()
@@ -44,35 +41,17 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
public void UpdateState(SalvageExpeditionConsoleState state) public void UpdateState(SalvageExpeditionConsoleState state)
{ {
_claimed = state.Claimed; _claimed = state.Claimed;
_cooldown = state.Cooldown;
_nextOffer = state.NextOffer; _nextOffer = state.NextOffer;
Container.DisposeAllChildren(); Container.DisposeAllChildren();
for (var i = 0; i < state.Missions.Count; i++) for (var i = 0; i < state.Missions.Count; i++)
{ {
// TODO: Make this XAML var missionParams = state.Missions[i];
var mission = state.Missions[i]; var config = missionParams.MissionType;
var config = _prototype.Index<SalvageExpeditionPrototype>(mission.Config); var mission = _salvage.GetMission(missionParams.MissionType, missionParams.Difficulty, missionParams.Seed);
var dungeonConfig = _prototype.Index<DungeonConfigPrototype>(config.DungeonConfigPrototype);
var faction = SharedSalvageSystem.GetFaction(config.Factions, mission.Seed);
var factionConfig = _prototype.Index<SalvageFactionPrototype>(faction);
// If we ever need this on server then move it // Mission title
var missionDesc = string.Empty;
var missionDetails = string.Empty;
switch (config.Mission)
{
case SalvageStructure structure:
var structureConfig = (SalvageStructureFaction) factionConfig.Configs[mission.Config];
missionDesc = "Demolition";
// TODO:
missionDetails = $"Destroy {SharedSalvageSystem.GetStructureCount(structure, mission.Seed)} {_prototype.Index<EntityPrototype>(structureConfig.Spawn).Name} structures.";
break;
default:
throw new NotImplementedException();
}
// Mission
var missionStripe = new StripeBack() var missionStripe = new StripeBack()
{ {
Margin = new Thickness(0f, -5f, 0f, 0f) Margin = new Thickness(0f, -5f, 0f, 0f)
@@ -80,7 +59,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
missionStripe.AddChild(new Label() missionStripe.AddChild(new Label()
{ {
Text = missionDesc, Text = Loc.GetString($"salvage-expedition-type-{config.ToString()}"),
HorizontalAlignment = HAlignment.Center, HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(0f, 5f, 0f, 5f), Margin = new Thickness(0f, 5f, 0f, 5f),
}); });
@@ -94,21 +73,27 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
// Details // Details
lBox.AddChild(new Label() 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: case DifficultyRating.None:
difficultyColor = StyleNano.ButtonColorDefault; difficultyColor = Color.FromHex("#52B4E996");
break; break;
case DifficultyRating.Minor: case DifficultyRating.Minor:
difficultyColor = StyleNano.GoodGreenFore; difficultyColor = Color.FromHex("#9FED5896");
break; break;
case DifficultyRating.Moderate: 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; break;
default: default:
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
@@ -116,21 +101,23 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = config.DifficultyRating.ToString(), Text = Loc.GetString($"salvage-expedition-difficulty-{missionParams.Difficulty.ToString()}"),
FontColorOverride = difficultyColor, FontColorOverride = difficultyColor,
HorizontalAlignment = HAlignment.Left, HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f), Margin = new Thickness(0f, 0f, 0f, 5f),
}); });
// Details // Details
var details = _salvage.GetMissionDescription(mission);
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = $"Details:" Text = Loc.GetString("salvage-expedition-window-details")
}); });
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = missionDetails, Text = details,
FontColorOverride = StyleNano.NanoGold, FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = HAlignment.Left, HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f), Margin = new Thickness(0f, 0f, 0f, 5f),
@@ -139,9 +126,11 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
// Details // Details
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = $"Hostiles:" Text = Loc.GetString("salvage-expedition-window-hostiles")
}); });
var faction = mission.Faction;
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = faction, Text = faction,
@@ -153,7 +142,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
// Duration // Duration
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = $"Duration:" Text = Loc.GetString("salvage-expedition-window-duration")
}); });
lBox.AddChild(new Label lBox.AddChild(new Label
@@ -167,133 +156,103 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
// Biome // Biome
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = "Biome:" Text = Loc.GetString("salvage-expedition-window-biome")
}); });
var biome = mission.Biome;
lBox.AddChild(new Label lBox.AddChild(new Label
{ {
Text = _prototype.Index<BiomePrototype>(config.Biome).Description, Text = Loc.GetString(_prototype.Index<SalvageBiomeMod>(biome).ID),
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,
FontColorOverride = StyleNano.NanoGold, FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = HAlignment.Left, HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f), Margin = new Thickness(0f, 0f, 0f, 5f),
}); });
// Modifiers // Modifiers
// TODO lBox.AddChild(new Label
// Rewards
lBox.AddChild(new Label()
{ {
Text = $"Reward:" Text = Loc.GetString("salvage-expedition-window-modifiers")
}); });
var salvageReward = SharedSalvageSystem.GetReward(_prototype.Index<WeightedRandomPrototype>(config.Reward), mission.Seed, _prototype); var mods = mission.Modifiers;
var difficulty = config.DifficultyRating;
var rewardDesc = string.Empty;
switch (salvageReward) lBox.AddChild(new Label
{ {
case BankReward bank: Text = string.Join("\n", mods.Select(o => "- " + o)).TrimEnd(),
rewardDesc = $"Bank payment of {(int) (bank.Amount * SharedSalvageSystem.GetDifficultyModifier(difficulty))}"; FontColorOverride = StyleNano.NanoGold,
break;
default:
throw new ArgumentOutOfRangeException();
}
lBox.AddChild(new Label()
{
Text = rewardDesc,
FontColorOverride = StyleNano.GoodGreenFore,
HorizontalAlignment = HAlignment.Left, HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f), Margin = new Thickness(0f, 0f, 0f, 5f),
}); });
lBox.AddChild(new Label() 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() lBox.AddChild(new Label()
{ {
Text = "N/A", Text = Loc.GetString("salvage-expedition-window-none"),
FontColorOverride = StyleNano.ConcerningOrangeFore, FontColorOverride = StyleNano.ConcerningOrangeFore,
HorizontalAlignment = HAlignment.Left, HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f), Margin = new Thickness(0f, 0f, 0f, 5f),
}); });
} }
else else
{
foreach (var lootProto in SharedSalvageSystem.GetLoot(config.Loots, mission.Seed, _prototype))
{ {
lBox.AddChild(new Label() lBox.AddChild(new Label()
{ {
Text = lootProto.Description, Text = string.Join("\n", mission.Loot.Select(o => "- " + _prototype.Index<SalvageLootPrototype>(o.Key).Description + (o.Value > 1 ? $" x {o.Value}" : ""))).TrimEnd(),
FontColorOverride = StyleNano.ConcerningOrangeFore, FontColorOverride = StyleNano.ConcerningOrangeFore,
HorizontalAlignment = HAlignment.Left, HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f), Margin = new Thickness(0f, 0f, 0f, 5f),
}); });
} }
}
// Claim // Claim
var claimButton = new Button() var claimButton = new Button()
{ {
HorizontalExpand = true, HorizontalExpand = true,
Pressed = state.ActiveMission == mission.Index, VerticalAlignment = VAlignment.Bottom,
Pressed = state.ActiveMission == missionParams.Index,
ToggleMode = true, ToggleMode = true,
Disabled = state.Claimed, Disabled = state.Claimed || state.Cooldown,
}; };
claimButton.Label.Margin = new Thickness(0f, 5f); claimButton.Label.Margin = new Thickness(0f, 5f);
claimButton.OnPressed += args => 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); claimButton.AddStyleClass(StyleBase.ButtonCaution);
} }
else 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)), PanelOverride = new StyleBoxFlat(new Color(30, 30, 34)),
HorizontalExpand = true, HorizontalExpand = true,
Margin = new Thickness(5f, 0f), Margin = new Thickness(5f, 0f),
Children = Children =
{ {
new BoxContainer() new BoxContainer
{ {
Orientation = BoxContainer.LayoutOrientation.Vertical, Orientation = BoxContainer.LayoutOrientation.Vertical,
Children = Children =
{ {
missionStripe, missionStripe,
lBox, lBox,
new Control() {VerticalExpand = true},
claimButton, claimButton,
}, },
Margin = new Thickness(5f, 5f) Margin = new Thickness(5f, 5f)
@@ -314,7 +273,7 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
if (_claimed) if (_claimed)
{ {
NextOfferBar.Value = 0f; NextOfferBar.Value = 0f;
NextOfferText.Text = "N/A"; NextOfferText.Text = "00:00";
return; return;
} }
@@ -327,7 +286,11 @@ public sealed partial class SalvageExpeditionWindow : FancyWindow,
} }
else 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}"; NextOfferText.Text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
} }
} }

View File

@@ -23,7 +23,6 @@ using Content.Shared.Administration;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers; using Content.Shared.Administration.Managers;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Module;
namespace Content.Server.IoC namespace Content.Server.IoC
{ {

View File

@@ -53,7 +53,7 @@ public sealed class PlanetCommand : IConsoleCommand
return; return;
} }
if (!_protoManager.HasIndex<BiomePrototype>(args[1])) if (!_protoManager.TryIndex<BiomeTemplatePrototype>(args[1], out var biomeTemplate))
{ {
shell.WriteError(Loc.GetString("cmd-planet-map-prototype", ("prototype", args[1]))); shell.WriteError(Loc.GetString("cmd-planet-map-prototype", ("prototype", args[1])));
return; return;
@@ -63,8 +63,9 @@ public sealed class PlanetCommand : IConsoleCommand
MetaDataComponent? metadata = null; MetaDataComponent? metadata = null;
var biome = _entManager.EnsureComponent<BiomeComponent>(mapUid); var biome = _entManager.EnsureComponent<BiomeComponent>(mapUid);
_entManager.System<BiomeSystem>().SetPrototype(biome, args[1]); var biomeSystem = _entManager.System<BiomeSystem>();
_entManager.System<BiomeSystem>().SetSeed(biome, _random.Next()); biomeSystem.SetSeed(biome, _random.Next());
biomeSystem.SetTemplate(biome, biomeTemplate);
_entManager.Dirty(biome); _entManager.Dirty(biome);
var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid); var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
@@ -106,7 +107,7 @@ public sealed class PlanetCommand : IConsoleCommand
if (args.Length == 2) if (args.Length == 2)
{ {
var options = _protoManager.EnumeratePrototypes<BiomePrototype>() var options = _protoManager.EnumeratePrototypes<BiomeTemplatePrototype>()
.Select(o => new CompletionOption(o.ID, "Biome")); .Select(o => new CompletionOption(o.ID, "Biome"));
return CompletionResult.FromOptions(options); return CompletionResult.FromOptions(options);
} }

View File

@@ -162,8 +162,9 @@ public sealed class HTNSystem : EntitySystem
public void UpdateNPC(ref int count, int maxUpdates, float frameTime) public void UpdateNPC(ref int count, int maxUpdates, float frameTime)
{ {
_planQueue.Process(); _planQueue.Process();
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
foreach (var (_, comp) in EntityQuery<ActiveNPCComponent, HTNComponent>()) while(query.MoveNext(out var uid, out _, out var comp))
{ {
// If we're over our max count or it's not MapInit then ignore the NPC. // If we're over our max count or it's not MapInit then ignore the NPC.
if (count >= maxUpdates) if (count >= maxUpdates)
@@ -173,10 +174,10 @@ public sealed class HTNSystem : EntitySystem
{ {
if (comp.PlanningJob.Exception != null) if (comp.PlanningJob.Exception != null)
{ {
_sawmill.Fatal($"Received exception on planning job for {comp.Owner}!"); _sawmill.Fatal($"Received exception on planning job for {uid}!");
_npc.SleepNPC(comp.Owner); _npc.SleepNPC(uid);
var exc = comp.PlanningJob.Exception; var exc = comp.PlanningJob.Exception;
RemComp<HTNComponent>(comp.Owner); RemComp<HTNComponent>(uid);
throw exc; throw exc;
} }
@@ -231,7 +232,7 @@ public sealed class HTNSystem : EntitySystem
RaiseNetworkEvent(new HTNMessage() RaiseNetworkEvent(new HTNMessage()
{ {
Uid = comp.Owner, Uid = uid,
Text = text.ToString(), Text = text.ToString(),
}, session.ConnectedClient); }, session.ConnectedClient);
} }

View File

@@ -0,0 +1,172 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Parallax.Biomes.Layers;
using Content.Shared.Parallax.Biomes.Markers;
using Robust.Shared.Console;
using Robust.Shared.Map;
namespace Content.Server.Parallax;
public sealed partial class BiomeSystem
{
private void InitializeCommands()
{
_console.RegisterCommand("biome_clear", Loc.GetString("cmd-biome_clear-desc"), Loc.GetString("cmd-biome_clear-help"), BiomeClearCallback, BiomeClearCallbackHelper);
_console.RegisterCommand("biome_addlayer", Loc.GetString("cmd-biome_addlayer-desc"), Loc.GetString("cmd-biome_addlayer-help"), AddLayerCallback, AddLayerCallbackHelp);
_console.RegisterCommand("biome_addmarkerlayer", Loc.GetString("cmd-biome_addmarkerlayer-desc"), Loc.GetString("cmd-biome_addmarkerlayer-desc"), AddMarkerLayerCallback, AddMarkerLayerCallbackHelper);
}
[AdminCommand(AdminFlags.Fun)]
private void BiomeClearCallback(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 1)
{
return;
}
int.TryParse(args[0], out var mapInt);
var mapId = new MapId(mapInt);
if (_mapManager.MapExists(mapId) ||
!TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
{
return;
}
ClearTemplate(biome);
}
private CompletionResult BiomeClearCallbackHelper(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(CompletionHelper.Components<BiomeComponent>(args[0], EntityManager), "Biome");
}
return CompletionResult.Empty;
}
[AdminCommand(AdminFlags.Fun)]
private void AddLayerCallback(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length < 3 || args.Length > 4)
{
return;
}
if (!int.TryParse(args[0], out var mapInt))
{
return;
}
var mapId = new MapId(mapInt);
if (!_mapManager.MapExists(mapId) || !TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
{
return;
}
if (!_proto.TryIndex<BiomeTemplatePrototype>(args[1], out var template))
{
return;
}
var offset = 0;
if (args.Length == 4)
{
int.TryParse(args[3], out offset);
}
AddTemplate(biome, args[2], template, offset);
}
private CompletionResult AddLayerCallbackHelp(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map ID");
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(
CompletionHelper.PrototypeIDs<BiomeTemplatePrototype>(proto: _proto), "Biome template");
}
if (args.Length == 3)
{
if (int.TryParse(args[0], out var mapInt))
{
var mapId = new MapId(mapInt);
if (TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
{
var results = new List<string>();
foreach (var layer in biome.Layers)
{
if (layer is not BiomeDummyLayer dummy)
continue;
results.Add(dummy.ID);
}
return CompletionResult.FromHintOptions(results, "Dummy layer ID");
}
}
}
if (args.Length == 4)
{
return CompletionResult.FromHint("Seed offset");
}
return CompletionResult.Empty;
}
[AdminCommand(AdminFlags.Fun)]
private void AddMarkerLayerCallback(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 2)
{
return;
}
if (!int.TryParse(args[0], out var mapInt))
{
return;
}
var mapId = new MapId(mapInt);
if (!_mapManager.MapExists(mapId) || !TryComp<BiomeComponent>(_mapManager.GetMapEntityId(mapId), out var biome))
{
return;
}
if (!_proto.HasIndex<BiomeMarkerLayerPrototype>(args[1]))
{
return;
}
biome.MarkerLayers.Add(args[1]);
}
private CompletionResult AddMarkerLayerCallbackHelper(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(CompletionHelper.Components<BiomeComponent>(args[0], EntityManager), "Biome");
}
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(
CompletionHelper.PrototypeIDs<BiomeMarkerLayerPrototype>(proto: _proto), "Marker");
}
return CompletionResult.Empty;
}
}

View File

@@ -1,21 +1,31 @@
using Content.Server.Decals; using Content.Server.Decals;
using Content.Server.Shuttles.Events;
using Content.Shared.Decals; using Content.Shared.Decals;
using Content.Shared.Parallax.Biomes; 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.Server.Player;
using Robust.Shared; using Robust.Shared;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Noise; using Robust.Shared.Noise;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Parallax; namespace Content.Server.Parallax;
public sealed class BiomeSystem : SharedBiomeSystem public sealed partial class BiomeSystem : SharedBiomeSystem
{ {
[Dependency] private readonly IConfigurationManager _configManager = default!; [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 IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DecalSystem _decals = default!; [Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
@@ -23,6 +33,10 @@ public sealed class BiomeSystem : SharedBiomeSystem
private readonly HashSet<EntityUid> _handledEntities = new(); private readonly HashSet<EntityUid> _handledEntities = new();
private const float DefaultLoadRange = 16f; private const float DefaultLoadRange = 16f;
private float _loadRange = DefaultLoadRange; private float _loadRange = DefaultLoadRange;
/// <summary>
/// Load area for chunks containing tiles, decals etc.
/// </summary>
private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange); private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange);
/// <summary> /// <summary>
@@ -30,18 +44,41 @@ public sealed class BiomeSystem : SharedBiomeSystem
/// </summary> /// </summary>
private readonly Dictionary<BiomeComponent, HashSet<Vector2i>> _activeChunks = new(); private readonly Dictionary<BiomeComponent, HashSet<Vector2i>> _activeChunks = new();
private readonly Dictionary<BiomeComponent,
Dictionary<string, HashSet<Vector2i>>> _markerChunks = new();
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<BiomeComponent, ComponentStartup>(OnBiomeStartup); SubscribeLocalEvent<BiomeComponent, ComponentStartup>(OnBiomeStartup);
SubscribeLocalEvent<BiomeComponent, MapInitEvent>(OnBiomeMapInit); SubscribeLocalEvent<BiomeComponent, MapInitEvent>(OnBiomeMapInit);
SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, SetLoadRange, true); _configManager.OnValueChanged(CVars.NetMaxUpdateRange, SetLoadRange, true);
InitializeCommands();
_proto.PrototypesReloaded += ProtoReload;
} }
public override void Shutdown() public override void Shutdown()
{ {
base.Shutdown(); base.Shutdown();
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, SetLoadRange); _configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, SetLoadRange);
_proto.PrototypesReloaded -= ProtoReload;
}
private void ProtoReload(PrototypesReloadedEventArgs obj)
{
if (!obj.ByType.TryGetValue(typeof(BiomeTemplatePrototype), out var reloads))
return;
var query = AllEntityQuery<BiomeComponent>();
while (query.MoveNext(out var biome))
{
if (biome.Template == null || !reloads.Modified.TryGetValue(biome.Template, out var proto))
continue;
SetTemplate(biome, (BiomeTemplatePrototype) proto);
}
} }
private void SetLoadRange(float obj) private void SetLoadRange(float obj)
@@ -61,15 +98,6 @@ public sealed class BiomeSystem : SharedBiomeSystem
SetSeed(component, _random.Next()); 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) public void SetSeed(BiomeComponent component, int seed)
{ {
component.Seed = seed; component.Seed = seed;
@@ -77,6 +105,120 @@ public sealed class BiomeSystem : SharedBiomeSystem
Dirty(component); Dirty(component);
} }
public void ClearTemplate(BiomeComponent component)
{
component.Layers.Clear();
component.Template = null;
Dirty(component);
}
/// <summary>
/// Sets the <see cref="BiomeComponent.Template"/> and refreshes layers.
/// </summary>
public void SetTemplate(BiomeComponent component, BiomeTemplatePrototype template)
{
component.Layers.Clear();
component.Template = template.ID;
foreach (var layer in template.Layers)
{
component.Layers.Add(layer);
}
Dirty(component);
}
/// <summary>
/// Adds the specified layer at the specified marker if it exists.
/// </summary>
public void AddLayer(BiomeComponent component, string id, IBiomeLayer addedLayer, int seedOffset = 0)
{
for (var i = 0; i < component.Layers.Count; i++)
{
var layer = component.Layers[i];
if (layer is not BiomeDummyLayer dummy || dummy.ID != id)
continue;
addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset);
component.Layers.Insert(i, addedLayer);
break;
}
Dirty(component);
}
public void AddMarkerLayer(BiomeComponent component, string marker)
{
if (!_proto.HasIndex<BiomeMarkerLayerPrototype>(marker))
{
// TODO: Log when we get a sawmill
return;
}
component.MarkerLayers.Add(marker);
Dirty(component);
}
/// <summary>
/// Adds the specified template at the specified marker if it exists, withour overriding every layer.
/// </summary>
public void AddTemplate(BiomeComponent component, string id, BiomeTemplatePrototype template, int seedOffset = 0)
{
for (var i = 0; i < component.Layers.Count; i++)
{
var layer = component.Layers[i];
if (layer is not BiomeDummyLayer dummy || dummy.ID != id)
continue;
for (var j = template.Layers.Count - 1; j >= 0; j--)
{
var addedLayer = template.Layers[j];
addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset);
component.Layers.Insert(i, addedLayer);
}
break;
}
Dirty(component);
}
private void OnFTLStarted(ref FTLStartedEvent ev)
{
var targetMap = ev.TargetCoordinates.ToMap(EntityManager, _transform);
var targetMapUid = _mapManager.GetMapEntityId(targetMap.MapId);
if (!TryComp<BiomeComponent>(targetMapUid, out var biome))
return;
var targetArea = new Box2(targetMap.Position - 64f, targetMap.Position + 64f);
Preload(targetMapUid, biome, targetArea);
}
/// <summary>
/// Preloads biome for the specified area.
/// </summary>
public void Preload(EntityUid uid, BiomeComponent component, Box2 area)
{
var markers = component.MarkerLayers;
var goobers = _markerChunks.GetOrNew(component);
foreach (var layer in markers)
{
var proto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
var enumerator = new ChunkIndicesEnumerator(area, proto.Size);
while (enumerator.MoveNext(out var chunk))
{
var chunkOrigin = chunk * proto.Size;
var layerChunks = goobers.GetOrNew(proto.ID);
layerChunks.Add(chunkOrigin.Value);
}
}
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
@@ -87,6 +229,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
while (biomes.MoveNext(out var biome)) while (biomes.MoveNext(out var biome))
{ {
_activeChunks.Add(biome, new HashSet<Vector2i>()); _activeChunks.Add(biome, new HashSet<Vector2i>());
_markerChunks.GetOrNew(biome);
} }
// Get chunks in range // Get chunks in range
@@ -98,7 +241,14 @@ public sealed class BiomeSystem : SharedBiomeSystem
_handledEntities.Add(pSession.AttachedEntity.Value) && _handledEntities.Add(pSession.AttachedEntity.Value) &&
biomeQuery.TryGetComponent(xform.MapUid, out var biome)) biomeQuery.TryGetComponent(xform.MapUid, out var biome))
{ {
AddChunksInRange(biome, _transform.GetWorldPosition(xform, xformQuery)); var worldPos = _transform.GetWorldPosition(xform, xformQuery);
AddChunksInRange(biome, worldPos);
foreach (var layer in biome.MarkerLayers)
{
var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
AddMarkerChunksInRange(biome, worldPos, layerProto);
}
} }
foreach (var viewer in pSession.ViewSubscriptions) foreach (var viewer in pSession.ViewSubscriptions)
@@ -110,16 +260,22 @@ public sealed class BiomeSystem : SharedBiomeSystem
continue; continue;
} }
AddChunksInRange(biome, _transform.GetWorldPosition(xform, xformQuery)); var worldPos = _transform.GetWorldPosition(xform, xformQuery);
AddChunksInRange(biome, worldPos);
foreach (var layer in biome.MarkerLayers)
{
var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
AddMarkerChunksInRange(biome, worldPos, layerProto);
}
} }
} }
var loadBiomes = AllEntityQuery<BiomeComponent, MapGridComponent>(); var loadBiomes = AllEntityQuery<BiomeComponent, MapGridComponent>();
while (loadBiomes.MoveNext(out var biome, out var grid)) while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
{ {
var noise = biome.Noise; var noise = biome.Noise;
var gridUid = grid.Owner;
// Load new chunks // Load new chunks
LoadChunks(biome, gridUid, grid, noise, xformQuery); LoadChunks(biome, gridUid, grid, noise, xformQuery);
@@ -129,6 +285,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
_handledEntities.Clear(); _handledEntities.Clear();
_activeChunks.Clear(); _activeChunks.Clear();
_markerChunks.Clear();
} }
private void AddChunksInRange(BiomeComponent biome, Vector2 worldPos) 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( private void LoadChunks(
BiomeComponent component, BiomeComponent component,
EntityUid gridUid, EntityUid gridUid,
@@ -148,8 +320,54 @@ public sealed class BiomeSystem : SharedBiomeSystem
FastNoiseLite noise, FastNoiseLite noise,
EntityQuery<TransformComponent> xformQuery) EntityQuery<TransformComponent> xformQuery)
{ {
var markers = _markerChunks[component];
var loadedMarkers = component.LoadedMarkers;
foreach (var (layer, chunks) in markers)
{
foreach (var chunk in chunks)
{
if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
continue;
var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
var buffer = layerProto.Radius / 2f;
mobChunks ??= new HashSet<Vector2i>();
mobChunks.Add(chunk);
loadedMarkers[layer] = mobChunks;
var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y);
// Load NOW
// TODO: Need poisson but crashes whenever I use moony's due to inputs or smth
var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius));
for (var i = 0; i < count; i++)
{
for (var j = 0; j < 5; j++)
{
var point = new Vector2(
chunk.X + buffer * rand.NextFloat() * (layerProto.Size - buffer),
chunk.Y + buffer * rand.NextFloat() * (layerProto.Size - buffer));
var coords = new EntityCoordinates(gridUid, point);
var tile = grid.LocalToTile(coords);
// Blocked spawn, try again.
if (grid.GetAnchoredEntitiesEnumerator(tile).MoveNext(out _))
continue;
for (var k = 0; k < layerProto.GroupCount; k++)
{
Spawn(layerProto.Prototype, new EntityCoordinates(gridUid, point));
}
break;
}
}
}
}
var active = _activeChunks[component]; var active = _activeChunks[component];
var prototype = ProtoManager.Index<BiomePrototype>(component.BiomePrototype);
List<(Vector2i, Tile)>? tiles = null; List<(Vector2i, Tile)>? tiles = null;
foreach (var chunk in active) foreach (var chunk in active)
@@ -159,7 +377,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
tiles ??= new List<(Vector2i, Tile)>(ChunkSize * ChunkSize); tiles ??= new List<(Vector2i, Tile)>(ChunkSize * ChunkSize);
// Load NOW! // 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, MapGridComponent grid,
Vector2i chunk, Vector2i chunk,
FastNoiseLite noise, FastNoiseLite noise,
BiomePrototype prototype,
List<(Vector2i, Tile)> tiles, List<(Vector2i, Tile)> tiles,
EntityQuery<TransformComponent> xformQuery) EntityQuery<TransformComponent> xformQuery)
{ {
@@ -191,7 +408,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
continue; continue;
// Pass in null so we don't try to get the tileref. // 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; continue;
tiles.Add((indices, biomeTile.Value)); tiles.Add((indices, biomeTile.Value));
@@ -217,7 +434,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
// Don't mess with anything that's potentially anchored. // Don't mess with anything that's potentially anchored.
var anchored = grid.GetAnchoredEntitiesEnumerator(indices); 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; continue;
// TODO: Fix non-anchored ents spawning. // 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. // At least for now unless we do lookups or smth, only work with anchoring.
if (xformQuery.TryGetComponent(ent, out var xform) && !xform.Anchored) 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); loadedEntities.Add(ent);
@@ -250,7 +467,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
// Don't mess with anything that's potentially anchored. // Don't mess with anything that's potentially anchored.
var anchored = grid.GetAnchoredEntitiesEnumerator(indices); 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; continue;
foreach (var decal in decals) 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) private void UnloadChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, FastNoiseLite noise)
{ {
var active = _activeChunks[component]; 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) private void UnloadChunk(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i chunk, FastNoiseLite noise, List<(Vector2i, Tile)> tiles)
{ {
// Reverse order to loading // Reverse order to loading
var prototype = ProtoManager.Index<BiomePrototype>(component.BiomePrototype);
component.ModifiedTiles.TryGetValue(chunk, out var modified); component.ModifiedTiles.TryGetValue(chunk, out var modified);
modified ??= new HashSet<Vector2i>(); modified ??= new HashSet<Vector2i>();
@@ -338,7 +558,7 @@ public sealed class BiomeSystem : SharedBiomeSystem
} }
// If it's default data unload the tile. // 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) grid.TryGetTileRef(indices, out var tileRef) && tileRef.Tile != biomeTile.Value)
{ {
modified.Add(indices); modified.Add(indices);
@@ -362,4 +582,6 @@ public sealed class BiomeSystem : SharedBiomeSystem
component.ModifiedTiles[chunk] = modified; component.ModifiedTiles[chunk] = modified;
} }
} }
#endregion
} }

View File

@@ -125,7 +125,10 @@ public sealed partial class DungeonJob
} }
var tiles = new List<(Vector2i, Tile)>(); var tiles = new List<(Vector2i, Tile)>();
var dungeon = new Dungeon(); var dungeon = new Dungeon()
{
Position = _position
};
var availablePacks = new List<DungeonRoomPackPrototype>(); var availablePacks = new List<DungeonRoomPackPrototype>();
var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count]; var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count];
var packTransforms = new Matrix3[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; return dungeon;
} }
} }

View File

@@ -110,13 +110,11 @@ public sealed partial class DungeonJob
var rooms = new List<DungeonRoom>(dungeon.Rooms); var rooms = new List<DungeonRoom>(dungeon.Rooms);
var roomTiles = new List<Vector2i>(); var roomTiles = new List<Vector2i>();
var tileData = new Tile(_tileDefManager[gen.Tile].TileId); 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 roomIndex = random.Next(rooms.Count);
var room = rooms[roomIndex]; var room = rooms[roomIndex];
rooms.RemoveAt(roomIndex);
// Move out 3 tiles in a direction away from center of the room // Move out 3 tiles in a direction away from center of the room
// If none of those intersect another tile it's probably external // 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) 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 direction = (tile - room.Center).ToAngle().GetCardinalDir().ToAngle().ToVec();
var isValid = true; var isValid = true;
@@ -163,8 +155,6 @@ public sealed partial class DungeonJob
_entManager.SpawnEntity(ent, gridCoords); _entManager.SpawnEntity(ent, gridCoords);
} }
count--;
// Clear out any biome tiles nearby to avoid blocking it // Clear out any biome tiles nearby to avoid blocking it
foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false)) foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false))
{ {

View File

@@ -28,7 +28,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
private readonly DungeonConfigPrototype _gen; private readonly DungeonConfigPrototype _gen;
private readonly int _seed; private readonly int _seed;
private readonly Vector2 _position; private readonly Vector2i _position;
private readonly MapGridComponent _grid; private readonly MapGridComponent _grid;
private readonly EntityUid _gridUid; private readonly EntityUid _gridUid;
@@ -51,7 +51,7 @@ public sealed partial class DungeonJob : Job<Dungeon>
MapGridComponent grid, MapGridComponent grid,
EntityUid gridUid, EntityUid gridUid,
int seed, int seed,
Vector2 position, Vector2i position,
CancellationToken cancellation = default) : base(maxTime, cancellation) CancellationToken cancellation = default) : base(maxTime, cancellation)
{ {
_sawmill = sawmill; _sawmill = sawmill;

View File

@@ -43,7 +43,7 @@ public sealed partial class DungeonSystem
return; return;
} }
var position = new Vector2(posX, posY); var position = new Vector2i(posX, posY);
var dungeonUid = _mapManager.GetMapEntityId(mapId); var dungeonUid = _mapManager.GetMapEntityId(mapId);
if (!TryComp<MapGridComponent>(dungeonUid, out var dungeonGrid)) if (!TryComp<MapGridComponent>(dungeonUid, out var dungeonGrid))

View File

@@ -161,7 +161,7 @@ public sealed partial class DungeonSystem : EntitySystem
public void GenerateDungeon(DungeonConfigPrototype gen, public void GenerateDungeon(DungeonConfigPrototype gen,
EntityUid gridUid, EntityUid gridUid,
MapGridComponent grid, MapGridComponent grid,
Vector2 position, Vector2i position,
int seed) int seed)
{ {
var cancelToken = new CancellationTokenSource(); var cancelToken = new CancellationTokenSource();
@@ -193,7 +193,7 @@ public sealed partial class DungeonSystem : EntitySystem
DungeonConfigPrototype gen, DungeonConfigPrototype gen,
EntityUid gridUid, EntityUid gridUid,
MapGridComponent grid, MapGridComponent grid,
Vector2 position, Vector2i position,
int seed) int seed)
{ {
var cancelToken = new CancellationTokenSource(); var cancelToken = new CancellationTokenSource();

View File

@@ -0,0 +1,44 @@
using Content.Shared.Salvage;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Salvage.Expeditions;
/// <summary>
/// Designates this entity as holding a salvage expedition.
/// </summary>
[RegisterComponent]
public sealed class SalvageExpeditionComponent : Component
{
public SalvageMissionParams MissionParams = default!;
/// <summary>
/// Where the dungeon is located for initial announcement.
/// </summary>
[DataField("dungeonLocation")]
public Vector2 DungeonLocation = Vector2.Zero;
/// <summary>
/// When the expeditions ends.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan EndTime;
/// <summary>
/// Station whose mission this is.
/// </summary>
[ViewVariables, DataField("station")]
public EntityUid Station;
[ViewVariables] public bool Completed = false;
[ViewVariables(VVAccess.ReadWrite), DataField("stage")]
public ExpeditionStage Stage = ExpeditionStage.Added;
}
public enum ExpeditionStage : byte
{
Added,
Running,
Countdown,
FinalCountdown,
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Salvage.Expeditions;
/// <summary>
/// Tracks expedition data for <see cref="SalvageMissionType.Mining"/>
/// </summary>
[RegisterComponent, Access(typeof(SalvageSystem))]
public sealed class SalvageMiningExpeditionComponent : Component
{
/// <summary>
/// Entities that were present on the shuttle and match the loot tax.
/// </summary>
[DataField("exemptEntities")]
public List<EntityUid> ExemptEntities = new();
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Salvage.Expeditions.Structure;
/// <summary>
/// Mission objective for salvage expeditions.
/// </summary>
[RegisterComponent, Access(typeof(SalvageSystem))]
public sealed class SalvageStructureComponent : Component
{
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.Salvage;
namespace Content.Server.Salvage.Expeditions.Structure;
/// <summary>
/// Tracks expedition data for <see cref="SalvageMissionType.Structure"/>
/// </summary>
[RegisterComponent, Access(typeof(SalvageSystem), typeof(SpawnSalvageMissionJob))]
public sealed class SalvageStructureExpeditionComponent : Component
{
[DataField("structures")]
public readonly List<EntityUid> Structures = new();
}

View File

@@ -0,0 +1,67 @@
using Content.Shared.Salvage;
using Robust.Server.GameObjects;
namespace Content.Server.Salvage;
public sealed partial class SalvageSystem
{
private void OnSalvageClaimMessage(EntityUid uid, SalvageExpeditionConsoleComponent component, ClaimSalvageMessage args)
{
var station = _station.GetOwningStation(uid);
if (!TryComp<SalvageExpeditionDataComponent>(station, out var data) || data.Claimed)
return;
if (!data.Missions.TryGetValue(args.Index, out var missionparams))
return;
SpawnMission(missionparams, station.Value);
data.ActiveMission = args.Index;
var mission = GetMission(missionparams.MissionType, missionparams.Difficulty, missionparams.Seed);
data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
UpdateConsoles(data);
}
private void OnSalvageConsoleInit(EntityUid uid, SalvageExpeditionConsoleComponent component, ComponentInit args)
{
UpdateConsole(component);
}
private void OnSalvageConsoleParent(EntityUid uid, SalvageExpeditionConsoleComponent component, ref EntParentChangedMessage args)
{
UpdateConsole(component);
}
private void UpdateConsoles(SalvageExpeditionDataComponent component)
{
var state = GetState(component);
foreach (var (console, xform, uiComp) in EntityQuery<SalvageExpeditionConsoleComponent, TransformComponent, ServerUserInterfaceComponent>(true))
{
var station = _station.GetOwningStation(console.Owner, xform);
if (station != component.Owner)
continue;
_ui.TrySetUiState(console.Owner, SalvageConsoleUiKey.Expedition, state, ui: uiComp);
}
}
private void UpdateConsole(SalvageExpeditionConsoleComponent component)
{
var station = _station.GetOwningStation(component.Owner);
SalvageExpeditionConsoleState state;
if (TryComp<SalvageExpeditionDataComponent>(station, out var dataComponent))
{
state = GetState(dataComponent);
}
else
{
state = new SalvageExpeditionConsoleState(TimeSpan.Zero, false, true, 0, new List<SalvageMissionParams>());
}
_ui.TrySetUiState(component.Owner, SalvageConsoleUiKey.Expedition, state);
}
}

View File

@@ -0,0 +1,241 @@
using System.Linq;
using System.Threading;
using Content.Server.CPUJob.JobQueues;
using Content.Server.CPUJob.JobQueues.Queues;
using Content.Server.Salvage.Expeditions;
using Content.Server.Salvage.Expeditions.Structure;
using Content.Server.Station.Systems;
using Content.Shared.Examine;
using Content.Shared.Salvage;
namespace Content.Server.Salvage;
public sealed partial class SalvageSystem
{
/*
* Handles setup / teardown of salvage expeditions.
*/
private const int MissionLimit = 5;
private readonly JobQueue _salvageQueue = new();
private readonly List<(SpawnSalvageMissionJob Job, CancellationTokenSource CancelToken)> _salvageJobs = new();
private const double SalvageJobTime = 0.002;
private void InitializeExpeditions()
{
SubscribeLocalEvent<StationInitializedEvent>(OnSalvageExpStationInit);
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, ComponentInit>(OnSalvageConsoleInit);
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, EntParentChangedMessage>(OnSalvageConsoleParent);
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, ClaimSalvageMessage>(OnSalvageClaimMessage);
SubscribeLocalEvent<SalvageExpeditionDataComponent, EntityUnpausedEvent>(OnDataUnpaused);
SubscribeLocalEvent<SalvageExpeditionComponent, ComponentShutdown>(OnExpeditionShutdown);
SubscribeLocalEvent<SalvageExpeditionComponent, EntityUnpausedEvent>(OnExpeditionUnpaused);
SubscribeLocalEvent<SalvageStructureComponent, ExaminedEvent>(OnStructureExamine);
}
private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent component, ComponentShutdown args)
{
foreach (var (job, cancelToken) in _salvageJobs.ToArray())
{
if (job.Station == component.Station)
{
cancelToken.Cancel();
_salvageJobs.Remove((job, cancelToken));
}
}
if (Deleted(component.Station))
return;
// Finish mission
if (TryComp<SalvageExpeditionDataComponent>(component.Station, out var data))
{
FinishExpedition(data, component, null);
}
}
private void OnDataUnpaused(EntityUid uid, SalvageExpeditionDataComponent component, ref EntityUnpausedEvent args)
{
component.NextOffer += args.PausedTime;
}
private void OnExpeditionUnpaused(EntityUid uid, SalvageExpeditionComponent component, ref EntityUnpausedEvent args)
{
component.EndTime += args.PausedTime;
}
private void OnSalvageExpStationInit(StationInitializedEvent ev)
{
EnsureComp<SalvageExpeditionDataComponent>(ev.Station);
}
private void UpdateExpeditions()
{
var currentTime = _timing.CurTime;
_salvageQueue.Process();
foreach (var (job, cancelToken) in _salvageJobs.ToArray())
{
switch (job.Status)
{
case JobStatus.Finished:
_salvageJobs.Remove((job, cancelToken));
break;
}
}
foreach (var comp in EntityQuery<SalvageExpeditionDataComponent>())
{
// Update offers
if (comp.NextOffer > currentTime || comp.Claimed)
continue;
comp.Cooldown = false;
comp.NextOffer += MissionCooldown;
GenerateMissions(comp);
UpdateConsoles(comp);
}
var query = EntityQueryEnumerator<SalvageExpeditionComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.EndTime < currentTime)
{
QueueDel(uid);
}
}
}
private void FinishExpedition(SalvageExpeditionDataComponent component, SalvageExpeditionComponent expedition, EntityUid? shuttle)
{
// Finish mission cleanup.
switch (expedition.MissionParams.MissionType)
{
// Handles the mining taxation.
case SalvageMissionType.Mining:
expedition.Completed = true;
if (shuttle != null && TryComp<SalvageMiningExpeditionComponent>(expedition.Owner, out var mining))
{
var xformQuery = GetEntityQuery<TransformComponent>();
var entities = new List<EntityUid>();
MiningTax(entities, shuttle.Value, mining, xformQuery);
var tax = GetMiningTax(expedition.MissionParams.Difficulty);
_random.Shuffle(entities);
// TODO: urgh this pr is already taking so long I'll do this later
for (var i = 0; i < Math.Ceiling(entities.Count * tax); i++)
{
// QueueDel(entities[i]);
}
}
break;
}
// Payout already handled elsewhere.
if (expedition.Completed)
{
_sawmill.Debug($"Completed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
component.NextOffer = _timing.CurTime + MissionCooldown;
Announce(expedition.Owner, Loc.GetString("salvage-expedition-mission-completed"));
}
else
{
_sawmill.Debug($"Failed mission {expedition.MissionParams.MissionType} with seed {expedition.MissionParams.Seed}");
component.NextOffer = _timing.CurTime + MissionFailedCooldown;
Announce(expedition.Owner, Loc.GetString("salvage-expedition-mission-failed"));
}
component.ActiveMission = 0;
component.Cooldown = true;
UpdateConsoles(component);
}
/// <summary>
/// Deducts ore tax for mining.
/// </summary>
private void MiningTax(List<EntityUid> entities, EntityUid entity, SalvageMiningExpeditionComponent mining, EntityQuery<TransformComponent> xformQuery)
{
if (!mining.ExemptEntities.Contains(entity))
{
entities.Add(entity);
}
var xform = xformQuery.GetComponent(entity);
var children = xform.ChildEnumerator;
while (children.MoveNext(out var child))
{
MiningTax(entities, child.Value, mining, xformQuery);
}
}
private void GenerateMissions(SalvageExpeditionDataComponent component)
{
component.Missions.Clear();
var configs = Enum.GetValues<SalvageMissionType>().ToList();
if (configs.Count == 0)
return;
for (var i = 0; i < MissionLimit; i++)
{
_random.Shuffle(configs);
var rating = (DifficultyRating) i;
foreach (var config in configs)
{
var mission = new SalvageMissionParams()
{
Index = component.NextIndex,
MissionType = config,
Seed = _random.Next(),
Difficulty = rating,
};
component.Missions[component.NextIndex++] = mission;
break;
}
}
}
private SalvageExpeditionConsoleState GetState(SalvageExpeditionDataComponent component)
{
var missions = component.Missions.Values.ToList();
return new SalvageExpeditionConsoleState(component.NextOffer, component.Claimed, component.Cooldown, component.ActiveMission, missions);
}
private void SpawnMission(SalvageMissionParams missionParams, EntityUid station)
{
var cancelToken = new CancellationTokenSource();
var job = new SpawnSalvageMissionJob(
SalvageJobTime,
EntityManager,
_timing,
_mapManager,
_prototypeManager,
_tileDefManager,
_biome,
_dungeon,
this,
station,
missionParams,
cancelToken.Token);
_salvageJobs.Add((job, cancelToken));
_salvageQueue.EnqueueJob(job);
}
private void OnStructureExamine(EntityUid uid, SalvageStructureComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("salvage-expedition-structure-examine"));
}
}

View File

@@ -0,0 +1,194 @@
using Content.Server.Salvage.Expeditions;
using Content.Server.Salvage.Expeditions.Structure;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Shared.Chat;
using Content.Shared.Salvage;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Salvage;
public sealed partial class SalvageSystem
{
/*
* Handles actively running a salvage expedition.
*/
private void InitializeRunner()
{
SubscribeLocalEvent<FTLRequestEvent>(OnFTLRequest);
SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
SubscribeLocalEvent<FTLCompletedEvent>(OnFTLCompleted);
}
/// <summary>
/// Announces status updates to salvage crewmembers on the state of the expedition.
/// </summary>
private void Announce(EntityUid mapUid, string text)
{
var mapId = Comp<MapComponent>(mapUid).MapId;
// I love TComms and chat!!!
_chat.ChatMessageToManyFiltered(
Filter.BroadcastMap(mapId),
ChatChannel.Radio,
text,
text,
_mapManager.GetMapEntityId(mapId),
false,
true,
null);
}
private void OnFTLRequest(ref FTLRequestEvent ev)
{
if (!HasComp<SalvageExpeditionComponent>(ev.MapUid) ||
!TryComp<FTLDestinationComponent>(ev.MapUid, out var dest))
{
return;
}
// Only one shuttle can occupy an expedition.
dest.Enabled = false;
_shuttleConsoles.RefreshShuttleConsoles();
}
private void OnFTLCompleted(ref FTLCompletedEvent args)
{
if (!TryComp<SalvageExpeditionComponent>(args.MapUid, out var component))
return;
// Someone FTLd there so start announcement
if (component.Stage != ExpeditionStage.Added)
return;
Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", (component.EndTime - _timing.CurTime).Minutes)));
if (component.DungeonLocation != Vector2.Zero)
Announce(args.MapUid, Loc.GetString("salvage-expedition-announcement-dungeon", ("direction", component.DungeonLocation.GetDir())));
component.Stage = ExpeditionStage.Running;
}
private void OnFTLStarted(ref FTLStartedEvent ev)
{
// Started a mining mission so work out exempt entities
if (TryComp<SalvageMiningExpeditionComponent>(
_mapManager.GetMapEntityId(ev.TargetCoordinates.ToMap(EntityManager, _transform).MapId),
out var mining))
{
var ents = new List<EntityUid>();
var xformQuery = GetEntityQuery<TransformComponent>();
MiningTax(ents, ev.Entity, mining, xformQuery);
mining.ExemptEntities = ents;
}
if (!TryComp<SalvageExpeditionComponent>(ev.FromMapUid, out var expedition) ||
!TryComp<SalvageExpeditionDataComponent>(expedition.Station, out var station))
{
return;
}
// Check if any shuttles remain.
var query = EntityQueryEnumerator<ShuttleComponent, TransformComponent>();
while (query.MoveNext(out _, out var xform))
{
if (xform.MapUid == ev.FromMapUid)
return;
}
// Last shuttle has left so finish the mission.
QueueDel(ev.FromMapUid.Value);
}
// Runs the expedition
private void UpdateRunner()
{
// Generic missions
var query = EntityQueryEnumerator<SalvageExpeditionComponent>();
// Run the basic mission timers (e.g. announcements, auto-FTL, completion, etc)
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Completed)
continue;
var remaining = comp.EndTime - _timing.CurTime;
if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(30))
{
comp.Stage = ExpeditionStage.FinalCountdown;
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-seconds", ("duration", TimeSpan.FromSeconds(30).Seconds)));
}
// TODO: Play song.
else if (comp.Stage < ExpeditionStage.Countdown && remaining < TimeSpan.FromMinutes(2))
{
comp.Stage = ExpeditionStage.Countdown;
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", TimeSpan.FromMinutes(2).Minutes)));
}
// Auto-FTL out any shuttles
else if (remaining < TimeSpan.FromSeconds(ShuttleSystem.DefaultStartupTime) + TimeSpan.FromSeconds(0.5))
{
var ftlTime = (float) remaining.TotalSeconds;
if (remaining < TimeSpan.FromSeconds(ShuttleSystem.DefaultStartupTime))
{
ftlTime = MathF.Max(0, (float) remaining.TotalSeconds - 0.5f);
}
ftlTime = MathF.Min(ftlTime, ShuttleSystem.DefaultStartupTime);
var shuttleQuery = AllEntityQuery<ShuttleComponent, TransformComponent>();
if (TryComp<StationDataComponent>(comp.Station, out var data))
{
foreach (var member in data.Grids)
{
while (shuttleQuery.MoveNext(out var shuttleUid, out var shuttle, out var shuttleXform))
{
if (shuttleXform.MapUid != uid || HasComp<FTLComponent>(shuttleUid))
continue;
_shuttle.FTLTravel(shuttleUid, shuttle, member, ftlTime);
}
break;
}
}
}
}
// Mining missions: NOOP
// Structure missions
var structureQuery = EntityQueryEnumerator<SalvageStructureExpeditionComponent, SalvageExpeditionComponent>();
while (structureQuery.MoveNext(out var uid, out var structure, out var comp))
{
if (comp.Completed)
continue;
var structureAnnounce = false;
for (var i = 0; i < structure.Structures.Count; i++)
{
var objective = structure.Structures[i];
if (Deleted(objective))
{
structure.Structures.RemoveSwap(i);
structureAnnounce = true;
}
}
if (structureAnnounce)
{
Announce(uid, Loc.GetString("salvage-expedition-structures-remaining", ("count", structure.Structures.Count)));
}
}
}
}

View File

@@ -16,10 +16,11 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Linq; using System.Linq;
using Content.Server.Cargo.Systems; using Content.Server.Chat.Managers;
using Content.Server.NPC.Pathfinding; using Content.Server.Chat.Systems;
using Content.Server.Parallax; using Content.Server.Parallax;
using Content.Server.Procedural; using Content.Server.Procedural;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -27,19 +28,22 @@ namespace Content.Server.Salvage
{ {
public sealed partial class SalvageSystem : SharedSalvageSystem public sealed partial class SalvageSystem : SharedSalvageSystem
{ {
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly BiomeSystem _biome = default!; [Dependency] private readonly BiomeSystem _biome = default!;
[Dependency] private readonly CargoSystem _cargo = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!; [Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly MapLoaderSystem _map = default!; [Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly PathfindingSystem _pathfinding = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly RadioSystem _radioSystem = default!; [Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = 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 StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _ui = 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 // Can't use RoundRestartCleanupEvent, I need to clean up before the grid, and components are gone to prevent the announcements
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd); SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
InitializeExpeditions();
InitializeRunner();
} }
private void OnRoundEnd(GameRunLevelChangedEvent ev) private void OnRoundEnd(GameRunLevelChangedEvent ev)
@@ -449,6 +456,9 @@ namespace Content.Server.Salvage
state.ActiveMagnets.Remove(magnet); state.ActiveMagnets.Remove(magnet);
} }
} }
UpdateExpeditions();
UpdateRunner();
} }
} }

View File

@@ -0,0 +1,372 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.CPUJob.JobQueues;
using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Salvage.Expeditions;
using Content.Server.Salvage.Expeditions.Structure;
using Content.Shared.Atmos;
using Content.Shared.Dataset;
using Content.Shared.Gravity;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Salvage;
using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers;
using Content.Shared.Storage;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Salvage;
public sealed class SpawnSalvageMissionJob : Job<bool>
{
private readonly IEntityManager _entManager;
private readonly IGameTiming _timing;
private readonly IMapManager _mapManager;
private readonly IPrototypeManager _prototypeManager;
private readonly ITileDefinitionManager _tileDefManager;
private readonly BiomeSystem _biome;
private readonly DungeonSystem _dungeon;
private readonly SalvageSystem _salvage;
public readonly EntityUid Station;
private readonly SalvageMissionParams _missionParams;
public SpawnSalvageMissionJob(
double maxTime,
IEntityManager entManager,
IGameTiming timing,
IMapManager mapManager,
IPrototypeManager protoManager,
ITileDefinitionManager tileDefManager,
BiomeSystem biome,
DungeonSystem dungeon,
SalvageSystem salvage,
EntityUid station,
SalvageMissionParams missionParams,
CancellationToken cancellation = default) : base(maxTime, cancellation)
{
_entManager = entManager;
_timing = timing;
_mapManager = mapManager;
_prototypeManager = protoManager;
_tileDefManager = tileDefManager;
_biome = biome;
_dungeon = dungeon;
_salvage = salvage;
Station = station;
_missionParams = missionParams;
}
protected override async Task<bool> Process()
{
Logger.DebugS("salvage", $"Spawning salvage mission with seed {_missionParams.Seed}");
var config = _missionParams.MissionType;
var mapId = _mapManager.CreateMap();
var mapUid = _mapManager.GetMapEntityId(mapId);
_mapManager.AddUninitializedMap(mapId);
MetaDataComponent? metadata = null;
var grid = _entManager.EnsureComponent<MapGridComponent>(mapUid);
var random = new Random(_missionParams.Seed);
// Setup mission configs
// As we go through the config the rating will deplete so we'll go for most important to least important.
var mission = _entManager.System<SharedSalvageSystem>()
.GetMission(_missionParams.MissionType, _missionParams.Difficulty, _missionParams.Seed);
var missionBiome = _prototypeManager.Index<SalvageBiomeMod>(mission.Biome);
if (missionBiome.BiomePrototype != null)
{
var biome = _entManager.AddComponent<BiomeComponent>(mapUid);
var biomeSystem = _entManager.System<BiomeSystem>();
biomeSystem.SetTemplate(biome, _prototypeManager.Index<BiomeTemplatePrototype>(missionBiome.BiomePrototype));
biomeSystem.SetSeed(biome, mission.Seed);
_entManager.Dirty(biome);
// Gravity
var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
gravity.Enabled = true;
_entManager.Dirty(gravity, metadata);
// Atmos
var atmos = _entManager.EnsureComponent<MapAtmosphereComponent>(mapUid);
atmos.Space = false;
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;
atmos.Mixture = new GasMixture(2500)
{
Temperature = 293.15f,
Moles = moles,
};
if (mission.Color != null)
{
var lighting = _entManager.EnsureComponent<MapLightComponent>(mapUid);
lighting.AmbientLightColor = mission.Color.Value;
_entManager.Dirty(lighting);
}
}
_mapManager.DoMapInitialize(mapId);
_mapManager.SetMapPaused(mapId, true);
// Setup expedition
var expedition = _entManager.AddComponent<SalvageExpeditionComponent>(mapUid);
expedition.Station = Station;
expedition.EndTime = _timing.CurTime + mission.Duration;
expedition.MissionParams = _missionParams;
// Don't want consoles to have the incorrect name until refreshed.
var ftlUid = _entManager.CreateEntityUninitialized("FTLPoint", new EntityCoordinates(mapUid, Vector2.Zero));
_entManager.GetComponent<MetaDataComponent>(ftlUid).EntityName = SharedSalvageSystem.GetFTLName(_prototypeManager.Index<DatasetPrototype>("names_borer"), _missionParams.Seed);
_entManager.InitializeAndStartEntity(ftlUid);
var landingPadRadius = 24;
var minDungeonOffset = landingPadRadius + 12;
var dungeonRotation = _dungeon.GetDungeonRotation(_missionParams.Seed);
var dungeonSpawnRotation = new Angle(random.NextDouble() * Math.Tau);
// If the dungeon were to spawn facing the landing pad then bump the offset a bit
// This isn't robust but fine for now.
if (Math.Abs((dungeonRotation - dungeonSpawnRotation).Theta) < Math.PI / 2)
{
minDungeonOffset += 16;
}
Dungeon dungeon = default!;
if (config != SalvageMissionType.Mining)
{
var maxDungeonOffset = minDungeonOffset + 24;
var dungeonOffsetDistance = minDungeonOffset + (maxDungeonOffset - minDungeonOffset) * random.NextFloat();
var dungeonOffset = new Vector2(dungeonOffsetDistance, 0f);
dungeonOffset = dungeonSpawnRotation.RotateVec(dungeonOffset);
var dungeonMod = _prototypeManager.Index<SalvageDungeonMod>(mission.Dungeon);
var dungeonConfig = _prototypeManager.Index<DungeonConfigPrototype>(dungeonMod.Proto);
dungeon =
await WaitAsyncTask(_dungeon.GenerateDungeonAsync(dungeonConfig, mapUid, grid, (Vector2i) dungeonOffset,
_missionParams.Seed));
// Aborty
if (dungeon.Rooms.Count == 0)
{
return false;
}
expedition.DungeonLocation = dungeonOffset;
}
List<Vector2i> reservedTiles = new();
// Setup the landing pad
var landingPadExtents = new Vector2i(landingPadRadius, landingPadRadius);
var tiles = new List<(Vector2i Indices, Tile Tile)>(landingPadExtents.X * landingPadExtents.Y * 2);
// Set the tiles themselves
var landingTile = new Tile(_tileDefManager["FloorSteel"].TileId);
foreach (var tile in grid.GetTilesIntersecting(new Circle(Vector2.Zero, landingPadRadius), false))
{
if (!_biome.TryGetBiomeTile(mapUid, grid, tile.GridIndices, out _))
continue;
tiles.Add((tile.GridIndices, landingTile));
reservedTiles.Add(tile.GridIndices);
}
grid.SetTiles(tiles);
// Mission setup
switch (config)
{
case SalvageMissionType.Mining:
await SetupMining(mission, mapUid);
break;
case SalvageMissionType.Destruction:
await SetupStructure(mission, dungeon, mapUid, grid, random);
break;
default:
throw new NotImplementedException();
}
// Handle loot
foreach (var (loot, count) in mission.Loot)
{
for (var i = 0; i < count; i++)
{
var lootProto = _prototypeManager.Index<SalvageLootPrototype>(loot);
await SpawnDungeonLoot(dungeon, lootProto, mapUid, grid, random, reservedTiles);
}
}
return true;
}
private async Task SpawnDungeonLoot(Dungeon? dungeon, SalvageLootPrototype loot, EntityUid gridUid, MapGridComponent grid, Random random, List<Vector2i> reservedTiles)
{
for (var i = 0; i < loot.LootRules.Count; i++)
{
var rule = loot.LootRules[i];
switch (rule)
{
case BiomeTemplateLoot biomeLoot:
if (_entManager.TryGetComponent<BiomeComponent>(gridUid, out var biome))
{
_biome.AddTemplate(biome, "Loot", _prototypeManager.Index<BiomeTemplatePrototype>(biomeLoot.Prototype), i);
}
break;
// Spawns a cluster (like an ore vein) nearby.
case DungeonClusterLoot clusterLoot:
await SpawnDungeonClusterLoot(dungeon!, clusterLoot, grid, random, reservedTiles);
break;
}
}
}
#region Loot
private async Task SpawnDungeonClusterLoot(
Dungeon dungeon,
DungeonClusterLoot loot,
MapGridComponent grid,
Random random,
List<Vector2i> reservedTiles)
{
var spawnTiles = new HashSet<Vector2i>();
for (var i = 0; i < loot.Points; i++)
{
var room = dungeon.Rooms[random.Next(dungeon.Rooms.Count)];
var clusterAmount = loot.ClusterAmount;
var spots = room.Tiles.ToList();
random.Shuffle(spots);
foreach (var spot in spots)
{
if (reservedTiles.Contains(spot))
continue;
var anchored = grid.GetAnchoredEntitiesEnumerator(spot);
if (anchored.MoveNext(out _))
{
continue;
}
clusterAmount--;
spawnTiles.Add(spot);
if (clusterAmount == 0)
break;
}
}
foreach (var tile in spawnTiles)
{
await SuspendIfOutOfTime();
var proto = _prototypeManager.Index<WeightedRandomPrototype>(loot.Prototype).Pick(random);
_entManager.SpawnEntity(proto, grid.GridTileToLocal(tile));
}
}
#endregion
#region Mission Specific
private async Task SetupMining(
SalvageMission mission,
EntityUid gridUid)
{
var faction = _prototypeManager.Index<SalvageFactionPrototype>(mission.Faction);
if (_entManager.TryGetComponent<BiomeComponent>(gridUid, out var biome))
{
// TODO: Better
for (var i = 0; i < _salvage.GetDifficulty(mission.Difficulty); i++)
{
_biome.AddMarkerLayer(biome, faction.Configs["Mining"]);
}
}
}
private async Task SetupStructure(
SalvageMission mission,
Dungeon dungeon,
EntityUid gridUid,
MapGridComponent grid,
Random random)
{
var structureComp = _entManager.EnsureComponent<SalvageStructureExpeditionComponent>(gridUid);
var availableRooms = dungeon.Rooms.ToList();
var faction = _prototypeManager.Index<SalvageFactionPrototype>(mission.Faction);
await SpawnMobsRandomRooms(mission, dungeon, faction, grid, random);
var structureCount = _salvage.GetStructureCount(mission.Difficulty);
var shaggy = faction.Configs["DefenseStructure"];
// Spawn the objectives
for (var i = 0; i < structureCount; i++)
{
var structureRoom = availableRooms[random.Next(availableRooms.Count)];
var spawnTile = structureRoom.Tiles.ElementAt(random.Next(structureRoom.Tiles.Count));
var uid = _entManager.SpawnEntity(shaggy, grid.GridTileToLocal(spawnTile));
_entManager.AddComponent<SalvageStructureComponent>(uid);
structureComp.Structures.Add(uid);
}
}
private async Task SpawnMobsRandomRooms(SalvageMission mission, Dungeon dungeon, SalvageFactionPrototype faction, MapGridComponent grid, Random random)
{
var groupSpawns = _salvage.GetSpawnCount(mission.Difficulty);
var groupSum = faction.MobGroups.Sum(o => o.Prob);
for (var i = 0; i < groupSpawns; i++)
{
var roll = random.NextFloat() * groupSum;
var value = 0f;
foreach (var group in faction.MobGroups)
{
value += group.Prob;
if (value < roll)
continue;
var mobGroupIndex = random.Next(faction.MobGroups.Count);
var mobGroup = faction.MobGroups[mobGroupIndex];
var spawnRoomIndex = random.Next(dungeon.Rooms.Count);
var spawnRoom = dungeon.Rooms[spawnRoomIndex];
var spawnTile = spawnRoom.Tiles.ElementAt(random.Next(spawnRoom.Tiles.Count));
var spawnPosition = grid.GridTileToLocal(spawnTile);
foreach (var entry in EntitySpawnCollection.GetSpawns(mobGroup.Entries, random))
{
_entManager.SpawnEntity(entry, spawnPosition);
}
await SuspendIfOutOfTime();
break;
}
}
}
#endregion
}

View File

@@ -6,4 +6,4 @@ namespace Content.Server.Shuttles.Events;
/// Raised when <see cref="ShuttleSystem.FasterThanLight"/> has completed FTL Travel. /// Raised when <see cref="ShuttleSystem.FasterThanLight"/> has completed FTL Travel.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public readonly record struct FTLCompletedEvent; public readonly record struct FTLCompletedEvent(EntityUid Entity, EntityUid MapUid);

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Shuttles.Events;
/// <summary>
/// Raised by a shuttle when it has requested an FTL.
/// </summary>
[ByRefEvent]
public record struct FTLRequestEvent(EntityUid MapUid);

View File

@@ -6,4 +6,4 @@ namespace Content.Server.Shuttles.Events;
/// Raised when a shuttle has moved to FTL space. /// Raised when a shuttle has moved to FTL space.
/// </summary> /// </summary>
[ByRefEvent] [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);

View File

@@ -150,6 +150,8 @@ public sealed partial class ShuttleSystem
hyperspace.Dock = false; hyperspace.Dock = false;
hyperspace.PriorityTag = priorityTag; hyperspace.PriorityTag = priorityTag;
_console.RefreshShuttleConsoles(); _console.RefreshShuttleConsoles();
var ev = new FTLRequestEvent(_mapManager.GetMapEntityId(coordinates.ToMap(EntityManager, _transform).MapId));
RaiseLocalEvent(shuttleUid, ref ev, true);
} }
/// <summary> /// <summary>
@@ -249,8 +251,10 @@ public sealed partial class ShuttleSystem
SetDockBolts(uid, true); SetDockBolts(uid, true);
_console.RefreshShuttleConsoles(uid); _console.RefreshShuttleConsoles(uid);
var ev = new FTLStartedEvent(fromMapUid, fromMatrix, fromRotation); var target = comp.TargetUid != null ? new EntityCoordinates(comp.TargetUid.Value, Vector2.Zero) : comp.TargetCoordinates;
RaiseLocalEvent(uid, ref ev);
var ev = new FTLStartedEvent(uid, target, fromMapUid, fromMatrix, fromRotation);
RaiseLocalEvent(uid, ref ev, true);
if (comp.TravelSound != null) if (comp.TravelSound != null)
{ {
@@ -344,7 +348,7 @@ public sealed partial class ShuttleSystem
comp.Accumulator += FTLCooldown; comp.Accumulator += FTLCooldown;
_console.RefreshShuttleConsoles(uid); _console.RefreshShuttleConsoles(uid);
_mapManager.SetMapPaused(mapId, false); _mapManager.SetMapPaused(mapId, false);
var ftlEvent = new FTLCompletedEvent(); var ftlEvent = new FTLCompletedEvent(uid, _mapManager.GetMapEntityId(mapId));
RaiseLocalEvent(uid, ref ftlEvent, true); RaiseLocalEvent(uid, ref ftlEvent, true);
break; break;
case FTLState.Cooldown: 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) public bool TryFTLProximity(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
{ {
if (!Resolve(targetUid, ref targetXform) || if (!Resolve(targetUid, ref targetXform) ||
targetXform.GridUid == null ||
targetXform.MapUid == null || targetXform.MapUid == null ||
!targetXform.MapUid.Value.IsValid() || !targetXform.MapUid.Value.IsValid() ||
!Resolve(shuttleUid, ref xform)) !Resolve(shuttleUid, ref xform))
@@ -592,6 +597,23 @@ public sealed partial class ShuttleSystem
spawnPos = _transform.GetWorldPosition(targetXform, xformQuery); spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
} }
// TODO: This is pretty crude for multiple landings.
if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid.Value))
{
var minRadius = (MathF.Max(targetAABB.Width, targetAABB.Height) + MathF.Max(shuttleAABB.Width, shuttleAABB.Height)) / 2f;
spawnPos = targetAABB.Center + _random.NextVector2(minRadius, minRadius + 64f);
}
else if (shuttleBody != null)
{
var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform, xformQuery);
var transform = new Transform(targetPos, targetRot);
spawnPos = Robust.Shared.Physics.Transform.Mul(transform, -shuttleBody.LocalCenter);
}
else
{
spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
}
xform.Coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos); xform.Coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos);
if (!HasComp<MapComponent>(targetXform.GridUid)) if (!HasComp<MapComponent>(targetXform.GridUid))

View File

@@ -1,6 +1,10 @@
using Content.Shared.Parallax.Biomes.Layers;
using Content.Shared.Parallax.Biomes.Markers;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Noise; using Robust.Shared.Noise;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; 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; namespace Content.Shared.Parallax.Biomes;
@@ -13,12 +17,22 @@ public sealed partial class BiomeComponent : Component
[AutoNetworkedField] [AutoNetworkedField]
public int Seed; public int Seed;
[ViewVariables(VVAccess.ReadWrite), /// <summary>
DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<BiomePrototype>))] /// The underlying entity, decal, and tile layers for the biome.
/// </summary>
[DataField("layers")]
[AutoNetworkedField] [AutoNetworkedField]
public string BiomePrototype = "Grasslands"; public List<IBiomeLayer> Layers = new();
// TODO: Need to flag tiles as not requiring custom data anymore, e.g. if we spawn an ent and don't unspawn it. /// <summary>
/// Templates to use for <see cref="Layers"/>. Optional as this can be set elsewhere.
/// </summary>
/// <remarks>
/// This is really just here for prototype reload support.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite),
DataField("template", customTypeSerializer: typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
public string? Template;
/// <summary> /// <summary>
/// If we've already generated a tile and couldn't deload it then we won't ever reload it in future. /// 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")] [DataField("loadedChunks")]
public readonly HashSet<Vector2i> LoadedChunks = new(); public readonly HashSet<Vector2i> LoadedChunks = new();
#region Markers
/// <summary> /// <summary>
/// Are we currently in the process of generating? /// Track what markers we've loaded already to avoid double-loading.
/// Used to flag modified tiles without callers having to deal with it.
/// </summary> /// </summary>
public bool Generating = false; [DataField("loadedMarkers", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<Vector2i>, BiomeMarkerLayerPrototype>))]
public readonly Dictionary<string, HashSet<Vector2i>> LoadedMarkers = new();
[DataField("markerLayers", customTypeSerializer: typeof(PrototypeIdListSerializer<BiomeMarkerLayerPrototype>))]
public List<string> MarkerLayers = new();
#endregion
} }

View File

@@ -1,102 +0,0 @@
using Content.Shared.Decals;
using Content.Shared.Maps;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Parallax.Biomes;
[Prototype("biome")]
public sealed class BiomePrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")]
public string Description = string.Empty;
[DataField("layers")]
public List<IBiomeLayer> Layers = new();
}
[ImplicitDataDefinitionForInheritors]
public interface IBiomeLayer
{
/// <summary>
/// Seed is used an offset from the relevant BiomeComponent's seed.
/// </summary>
FastNoiseLite Noise { get; }
/// <summary>
/// Threshold for this layer to be present. If set to 0 forces it for every tile.
/// </summary>
float Threshold { get; }
}
public sealed class BiomeTileLayer : IBiomeLayer
{
[DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
/// <inheritdoc/>
[DataField("threshold")]
public float Threshold { get; } = 0.5f;
/// <summary>
/// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
/// </summary>
[DataField("variants")]
public List<byte>? Variants = null;
[DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = string.Empty;
}
/// <summary>
/// Handles actual objects such as decals and entities.
/// </summary>
public interface IBiomeWorldLayer : IBiomeLayer
{
/// <summary>
/// What tiles we're allowed to spawn on, real or biome.
/// </summary>
List<string> AllowedTiles { get; }
}
public sealed class BiomeDecalLayer : IBiomeWorldLayer
{
/// <inheritdoc/>
[DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
public List<string> AllowedTiles { get; } = new();
/// <summary>
/// Divide each tile up by this amount.
/// </summary>
[DataField("divisions")]
public float Divisions = 1f;
[DataField("noise")]
public FastNoiseLite Noise { get; } = new(0);
/// <inheritdoc/>
[DataField("threshold")]
public float Threshold { get; } = 0.8f;
[DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DecalPrototype>))]
public List<string> Decals = new();
}
public sealed class BiomeEntityLayer : IBiomeWorldLayer
{
/// <inheritdoc/>
[DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
public List<string> AllowedTiles { get; } = new();
[DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
/// <inheritdoc/>
[DataField("threshold")]
public float Threshold { get; } = 0.5f;
[DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> Entities = new();
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Parallax.Biomes.Layers;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
namespace Content.Shared.Parallax.Biomes;
/// <summary>
/// A preset group of biome layers to be used for a <see cref="BiomeComponent"/>
/// </summary>
[Prototype("biomeTemplate")]
public sealed class BiomeTemplatePrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
[DataField("layers")]
public List<IBiomeLayer> Layers = new();
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Decals;
using Content.Shared.Maps;
using Robust.Shared.Noise;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Parallax.Biomes.Layers;
[Serializable, NetSerializable]
public sealed class BiomeDecalLayer : IBiomeWorldLayer
{
/// <inheritdoc/>
[DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
public List<string> AllowedTiles { get; } = new();
/// <summary>
/// Divide each tile up by this amount.
/// </summary>
[DataField("divisions")]
public float Divisions = 1f;
[DataField("noise")]
public FastNoiseLite Noise { get; } = new(0);
/// <inheritdoc/>
[DataField("threshold")]
public float Threshold { get; } = 0.8f;
/// <inheritdoc/>
[DataField("invert")] public bool Invert { get; } = false;
[DataField("decals", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer<DecalPrototype>))]
public List<string> Decals = new();
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Noise;
using Robust.Shared.Serialization;
namespace Content.Shared.Parallax.Biomes.Layers;
/// <summary>
/// Dummy layer that specifies a marker to be replaced by external code.
/// For example if they wish to add their own layers at specific points across different templates.
/// </summary>
[Serializable, NetSerializable]
public sealed class BiomeDummyLayer : IBiomeLayer
{
[DataField("id", required: true)] public string ID = string.Empty;
public FastNoiseLite Noise { get; } = new();
public float Threshold { get; }
public bool Invert { get; }
}

View File

@@ -0,0 +1,27 @@
using Content.Shared.Maps;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Parallax.Biomes.Layers;
[Serializable, NetSerializable]
public sealed class BiomeEntityLayer : IBiomeWorldLayer
{
/// <inheritdoc/>
[DataField("allowedTiles", customTypeSerializer:typeof(PrototypeIdListSerializer<ContentTileDefinition>))]
public List<string> AllowedTiles { get; } = new();
[DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
/// <inheritdoc/>
[DataField("threshold")]
public float Threshold { get; } = 0.5f;
/// <inheritdoc/>
[DataField("invert")] public bool Invert { get; } = false;
[DataField("entities", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> Entities = new();
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Maps;
using Robust.Shared.Noise;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Parallax.Biomes.Layers;
[Serializable, NetSerializable]
public sealed class BiomeTileLayer : IBiomeLayer
{
[DataField("noise")] public FastNoiseLite Noise { get; } = new(0);
/// <inheritdoc/>
[DataField("threshold")]
public float Threshold { get; } = 0.5f;
/// <inheritdoc/>
[DataField("invert")] public bool Invert { get; } = false;
/// <summary>
/// Which tile variants to use for this layer. Uses all of the tile's variants if none specified
/// </summary>
[DataField("variants")]
public List<byte>? Variants = null;
[DataField("tile", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
public string Tile = string.Empty;
}

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Noise;
namespace Content.Shared.Parallax.Biomes.Layers;
[ImplicitDataDefinitionForInheritors]
public interface IBiomeLayer
{
/// <summary>
/// Seed is used an offset from the relevant BiomeComponent's seed.
/// </summary>
FastNoiseLite Noise { get; }
/// <summary>
/// Threshold for this layer to be present. If set to 0 forces it for every tile.
/// </summary>
float Threshold { get; }
/// <summary>
/// Is the thresold inverted so we need to be lower than it.
/// </summary>
public bool Invert { get; }
}

View File

@@ -0,0 +1,12 @@
namespace Content.Shared.Parallax.Biomes.Layers;
/// <summary>
/// Handles actual objects such as decals and entities.
/// </summary>
public interface IBiomeWorldLayer : IBiomeLayer
{
/// <summary>
/// What tiles we're allowed to spawn on, real or biome.
/// </summary>
List<string> AllowedTiles { get; }
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Parallax.Biomes.Points;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Parallax.Biomes.Markers;
[Prototype("biomeMarkerLayer")]
public sealed class BiomeMarkerLayerPrototype : IBiomeMarkerLayer
{
[IdDataField] public string ID { get; } = default!;
[DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = string.Empty;
/// <inheritdoc />
[DataField("radius")]
public float Radius { get; } = 12f;
/// <summary>
/// How many mobs to spawn in one group.
/// </summary>
[DataField("groupCount")]
public int GroupCount = 1;
/// <inheritdoc />
[DataField("size")]
public int Size { get; } = 64;
}

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Parallax.Biomes.Points;
/// <summary>
/// Specifies one-off marker points to be used. This could be for dungeon markers, mob markers, etc.
/// These are run outside of the tile / decal / entity layers.
/// </summary>
public interface IBiomeMarkerLayer : IPrototype
{
/// <summary>
/// Minimum radius between 2 points
/// </summary>
[DataField("radius")]
public float Radius { get; }
/// <summary>
/// How large the pre-generated points area is.
/// </summary>
[DataField("size")]
public int Size { get; }
}

View File

@@ -1,11 +1,10 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.Maps; using Content.Shared.Maps;
using Robust.Shared.GameStates; using Content.Shared.Parallax.Biomes.Layers;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Noise; using Robust.Shared.Noise;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Shared.Parallax.Biomes; namespace Content.Shared.Parallax.Biomes;
@@ -72,7 +71,7 @@ public abstract class SharedBiomeSystem : EntitySystem
throw new ArgumentOutOfRangeException(); 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) if (grid.TryGetTileRef(indices, out var tileRef) && !tileRef.Tile.IsEmpty)
{ {
@@ -86,14 +85,13 @@ public abstract class SharedBiomeSystem : EntitySystem
return false; return false;
} }
return TryGetBiomeTile(indices, ProtoManager.Index<BiomePrototype>(biome.BiomePrototype), return TryGetBiomeTile(indices, biome.Layers, biome.Noise, grid, out tile);
biome.Noise, grid, out tile);
} }
/// <summary> /// <summary>
/// Tries to get the tile, real or otherwise, for the specified indices. /// Tries to get the tile, real or otherwise, for the specified indices.
/// </summary> /// </summary>
public bool TryGetBiomeTile(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile) public bool TryGetBiomeTile(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
{ {
if (grid?.TryGetTileRef(indices, out var tileRef) == true && !tileRef.Tile.IsEmpty) if (grid?.TryGetTileRef(indices, out var tileRef) == true && !tileRef.Tile.IsEmpty)
{ {
@@ -103,16 +101,16 @@ public abstract class SharedBiomeSystem : EntitySystem
var oldSeed = noise.GetSeed(); 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) if (layer is not BiomeTileLayer tileLayer)
continue; continue;
SetNoise(noise, oldSeed, layer.Noise); SetNoise(noise, oldSeed, layer.Noise);
if (TryGetTile(indices, noise, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile)) if (TryGetTile(indices, noise, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
{ {
noise.SetSeed(oldSeed); noise.SetSeed(oldSeed);
return true; return true;
@@ -127,9 +125,10 @@ public abstract class SharedBiomeSystem : EntitySystem
/// <summary> /// <summary>
/// Gets the underlying biome tile, ignoring any existing tile that may be there. /// Gets the underlying biome tile, ignoring any existing tile that may be there.
/// </summary> /// </summary>
private bool TryGetTile(Vector2i indices, FastNoiseLite seed, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile) private bool TryGetTile(Vector2i indices, FastNoiseLite seed, bool invert, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
{ {
var found = seed.GetNoise(indices.X, indices.Y); var found = seed.GetNoise(indices.X, indices.Y);
found = invert ? found * -1 : found;
if (found < threshold) if (found < threshold)
{ {
@@ -159,10 +158,10 @@ public abstract class SharedBiomeSystem : EntitySystem
/// <summary> /// <summary>
/// Tries to get the relevant entity for this tile. /// Tries to get the relevant entity for this tile.
/// </summary> /// </summary>
protected bool TryGetEntity(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid, protected bool TryGetEntity(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent grid,
[NotNullWhen(true)] out string? entity) [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; entity = null;
return false; return false;
@@ -171,13 +170,15 @@ public abstract class SharedBiomeSystem : EntitySystem
var tileId = TileDefManager[tileRef.Value.TypeId].ID; var tileId = TileDefManager[tileRef.Value.TypeId].ID;
var oldSeed = noise.GetSeed(); 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. // Decals might block entity so need to check if there's one in front of us.
switch (layer) switch (layer)
{ {
case BiomeDummyLayer:
continue;
case IBiomeWorldLayer worldLayer: case IBiomeWorldLayer worldLayer:
if (!worldLayer.AllowedTiles.Contains(tileId)) if (!worldLayer.AllowedTiles.Contains(tileId))
continue; continue;
@@ -188,7 +189,9 @@ public abstract class SharedBiomeSystem : EntitySystem
} }
SetNoise(noise, oldSeed, layer.Noise); SetNoise(noise, oldSeed, layer.Noise);
var invert = layer.Invert;
var value = noise.GetNoise(indices.X, indices.Y); var value = noise.GetNoise(indices.X, indices.Y);
value = invert ? value * -1 : value;
if (value < layer.Threshold) if (value < layer.Threshold)
{ {
@@ -215,10 +218,10 @@ public abstract class SharedBiomeSystem : EntitySystem
/// <summary> /// <summary>
/// Tries to get the relevant decals for this tile. /// Tries to get the relevant decals for this tile.
/// </summary> /// </summary>
public bool TryGetDecals(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid, public bool TryGetDecals(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent grid,
[NotNullWhen(true)] out List<(string ID, Vector2 Position)>? decals) [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; decals = null;
return false; return false;
@@ -227,13 +230,15 @@ public abstract class SharedBiomeSystem : EntitySystem
var tileId = TileDefManager[tileRef.Value.TypeId].ID; var tileId = TileDefManager[tileRef.Value.TypeId].ID;
var oldSeed = noise.GetSeed(); 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. // Entities might block decal so need to check if there's one in front of us.
switch (layer) switch (layer)
{ {
case BiomeDummyLayer:
continue;
case IBiomeWorldLayer worldLayer: case IBiomeWorldLayer worldLayer:
if (!worldLayer.AllowedTiles.Contains(tileId)) if (!worldLayer.AllowedTiles.Contains(tileId))
continue; continue;
@@ -244,11 +249,15 @@ public abstract class SharedBiomeSystem : EntitySystem
} }
SetNoise(noise, oldSeed, layer.Noise); SetNoise(noise, oldSeed, layer.Noise);
var invert = layer.Invert;
// Check if the other layer should even render, if not then keep going. // Check if the other layer should even render, if not then keep going.
if (layer is not BiomeDecalLayer decalLayer) 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; continue;
decals = null; 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 index = new Vector2(indices.X + x * 1f / decalLayer.Divisions, indices.Y + y * 1f / decalLayer.Divisions);
var decalValue = noise.GetNoise(index.X, index.Y); var decalValue = noise.GetNoise(index.X, index.Y);
decalValue = invert ? decalValue * -1 : decalValue;
if (decalValue < decalLayer.Threshold) if (decalValue < decalLayer.Threshold)
continue; continue;

View File

@@ -2,6 +2,13 @@ namespace Content.Shared.Procedural;
public sealed class Dungeon public sealed class Dungeon
{ {
/// <summary>
/// Starting position used to generate the dungeon from.
/// </summary>
public Vector2i Position;
public Vector2i Center;
public List<DungeonRoom> Rooms = new(); public List<DungeonRoom> Rooms = new();
/// <summary> /// <summary>

View File

@@ -0,0 +1,13 @@
using Content.Shared.Parallax.Biomes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Procedural.Loot;
/// <summary>
/// Adds a biome template layer for dungeon loot.
/// </summary>
public sealed class BiomeTemplateLoot : IDungeonLoot
{
[DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
public string Prototype = string.Empty;
}

View File

@@ -1,35 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Procedural.Loot;
/// <summary>
/// Spawns loot at points in the specified rooms
/// </summary>
public sealed class ClusterLoot : IDungeonLoot
{
/// <summary>
/// Minimum spawns in a cluster.
/// </summary>
[DataField("minCluster")]
public int MinClusterAmount;
/// <summary>
/// Maximum spawns in a cluster.
/// </summary>
[DataField("maxCluster")] public int MaxClusterAmount;
/// <summary>
/// Amount to spawn for the entire loot.
/// </summary>
[DataField("max")]
public int Amount;
/// <summary>
/// Number of points to spawn.
/// </summary>
[DataField("points")] public int Points;
[DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype { get; } = string.Empty;
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Random;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Procedural.Loot;
/// <summary>
/// Spawns loot at points in the specified area inside of a dungeon room.
/// </summary>
public sealed class DungeonClusterLoot : IDungeonLoot
{
/// <summary>
/// Spawns in a cluster.
/// </summary>
[DataField("clusterAmount")]
public int ClusterAmount = 1;
/// <summary>
/// Number of clusters to spawn.
/// </summary>
[DataField("clusters")] public int Points = 1;
[DataField("lootTable", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
public string Prototype { get; } = string.Empty;
}

View File

@@ -3,5 +3,4 @@ namespace Content.Shared.Procedural.Loot;
[ImplicitDataDefinitionForInheritors] [ImplicitDataDefinitionForInheritors]
public interface IDungeonLoot public interface IDungeonLoot
{ {
string Prototype { get; }
} }

View File

@@ -1,4 +1,6 @@
using Content.Shared.Salvage;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.Loot; namespace Content.Shared.Procedural.Loot;
@@ -12,6 +14,12 @@ public sealed class SalvageLootPrototype : IPrototype
[DataField("desc")] public string Description = string.Empty; [DataField("desc")] public string Description = string.Empty;
/// <summary>
/// Mission types this loot is not allowed to spawn for
/// </summary>
[DataField("blacklist")]
public List<SalvageMissionType> Blacklist = new();
/// <summary> /// <summary>
/// All of the loot rules /// All of the loot rules
/// </summary> /// </summary>

View File

@@ -1,10 +0,0 @@
namespace Content.Shared.Procedural.Rewards;
/// <summary>
/// Payout to the station's bank account.
/// </summary>
public sealed class BankReward : ISalvageReward
{
[DataField("amount")]
public int Amount = 0;
}

View File

@@ -1,7 +0,0 @@
namespace Content.Shared.Procedural.Rewards;
[ImplicitDataDefinitionForInheritors]
public interface ISalvageReward
{
}

View File

@@ -1,14 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Procedural.Rewards;
/// <summary>
/// Given after successful completion of a salvage mission.
/// </summary>
[Prototype("salvageReward")]
public sealed class SalvageRewardPrototype : IPrototype
{
[IdDataField] public string ID { get; } = string.Empty;
[DataField("reward", required: true)] public ISalvageReward Reward = default!;
}

View File

@@ -1,21 +0,0 @@
namespace Content.Shared.Salvage.Expeditions.Extraction;
public sealed class SalvageExtraction : ISalvageMission
{
/// <summary>
/// Minimum weight to be used for a wave.
/// </summary>
[DataField("minWaveWeight")] public float MinWaveWeight = 5;
/// <summary>
/// Minimum time between 2 waves. Roughly the end of one to the start of another.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("waveCooldown")]
public TimeSpan WaveCooldown = TimeSpan.FromSeconds(60);
/// <summary>
/// How much weight accumulates per second while the expedition is active.
/// </summary>
[DataField("weightAccumulator")]
public float WeightAccumulator = 0.1f;
}

View File

@@ -1,7 +0,0 @@
namespace Content.Shared.Salvage.Expeditions;
public interface IFactionExpeditionConfig
{
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared.Salvage.Expeditions.Modifiers;
public interface ISalvageMod
{
/// <summary>
/// Player-friendly version describing this modifier.
/// </summary>
string Description { get; }
float Cost { get; }
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Parallax.Biomes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
/// <summary>
/// Affects the biome to be used for salvage.
/// </summary>
[Prototype("salvageBiomeMod")]
public sealed class SalvageBiomeMod : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
/// <summary>
/// Is weather allowed to apply to this biome.
/// </summary>
[DataField("weather")]
public bool Weather = true;
[DataField("biome", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
public string? BiomePrototype;
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Procedural;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
[Prototype("salvageDungeonMod")]
public sealed class SalvageDungeonMod : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
[DataField("proto", customTypeSerializer:typeof(PrototypeIdSerializer<DungeonConfigPrototype>))]
public string Proto = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
/// <summary>
/// Biomes this dungeon can occur in.
/// </summary>
[DataField("biomeMods", customTypeSerializer:typeof(PrototypeIdListSerializer<SalvageBiomeMod>))]
public List<string>? BiomeMods;
}

View File

@@ -0,0 +1,26 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
[Prototype("salvageLightMod")]
public sealed class SalvageLightMod : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
[DataField("color", required: true)] public Color? Color;
/// <summary>
/// Biomes that this color applies to.
/// </summary>
[DataField("biomes", customTypeSerializer: typeof(PrototypeIdListSerializer<SalvageBiomeMod>))]
public List<string>? Biomes;
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
/// <summary>
/// Generic modifiers with no additional data
/// </summary>
[Prototype("salvageMod")]
public sealed class SalvageMod : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
}

View File

@@ -0,0 +1,23 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
[Prototype("salvageTimeMod")]
public sealed class SalvageTimeMod : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
[DataField("minDuration")]
public int MinDuration = 600;
[DataField("maxDuration")]
public int MaxDuration = 660;
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Parallax.Biomes;
using Content.Shared.Weather;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Salvage.Expeditions.Modifiers;
[Prototype("salvageWeatherMod")]
public sealed class SalvageWeatherMod : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
[DataField("weather", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<WeatherPrototype>))]
public string WeatherPrototype = string.Empty;
/// <summary>
/// Whitelist for biomes. If empty assumed any allowed.
/// </summary>
[DataField("biomes", customTypeSerializer:typeof(PrototypeIdListSerializer<BiomeTemplatePrototype>))]
public List<string> Biomes = new();
}

View File

@@ -1,19 +1,28 @@
using Content.Shared.Salvage.Expeditions.Modifiers;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Salvage.Expeditions; namespace Content.Shared.Salvage.Expeditions;
[Prototype("salvageFaction")] [Prototype("salvageFaction")]
public sealed class SalvageFactionPrototype : IPrototype public sealed class SalvageFactionPrototype : IPrototype, ISalvageMod
{ {
[IdDataField] public string ID { get; } = default!; [IdDataField] public string ID { get; } = default!;
[DataField("desc")] public string Description { get; } = string.Empty;
/// <summary>
/// Cost for difficulty modifiers.
/// </summary>
[DataField("cost")]
public float Cost { get; } = 0f;
[ViewVariables(VVAccess.ReadWrite), DataField("groups", required: true)] [ViewVariables(VVAccess.ReadWrite), DataField("groups", required: true)]
public List<SalvageMobGroup> MobGroups = default!; public List<SalvageMobGroup> MobGroups = default!;
/// <summary> /// <summary>
/// Per expedition type data for this faction. /// Miscellaneous data for factions.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("configs", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<IFactionExpeditionConfig, SalvageExpeditionPrototype>))] [ViewVariables(VVAccess.ReadWrite), DataField("configs")]
public Dictionary<string, IFactionExpeditionConfig> Configs = new(); public Dictionary<string, string> Configs = new();
} }

View File

@@ -1,17 +0,0 @@
namespace Content.Shared.Salvage.Expeditions.Structure;
/// <summary>
/// Destroy the specified number of structures to finish the expedition.
/// </summary>
[DataDefinition]
public sealed class SalvageStructure : ISalvageMission
{
[DataField("desc")]
public string Description = string.Empty;
[ViewVariables(VVAccess.ReadWrite), DataField("minStructures")]
public int MinStructures = 3;
[ViewVariables(VVAccess.ReadWrite), DataField("maxStructures")]
public int MaxStructures = 5;
}

View File

@@ -1,23 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Salvage.Expeditions.Structure;
/// <summary>
/// Per-faction config for Salvage Structure expeditions.
/// </summary>
[DataDefinition]
public sealed class SalvageStructureFaction : IFactionExpeditionConfig
{
/// <summary>
/// Entity prototype of the structures to destroy.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("spawn", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Spawn = default!;
/// <summary>
/// How many groups of mobs to spawn.
/// </summary>
[DataField("groupCount")]
public int Groups = 5;
}

View File

@@ -1,3 +0,0 @@
namespace Content.Shared.Salvage;
public interface ISalvageMission {}

View File

@@ -1,89 +0,0 @@
using Content.Shared.Dataset;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot;
using Content.Shared.Procedural.Rewards;
using Content.Shared.Random;
using Content.Shared.Salvage.Expeditions;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Salvage;
[Prototype("salvageExpedition")]
public sealed class SalvageExpeditionPrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
/// <summary>
/// Naming scheme for the FTL marker.
/// </summary>
[DataField("nameProto", customTypeSerializer:typeof(PrototypeIdSerializer<DatasetPrototype>))]
public string NameProto = "names_borer";
/// <summary>
/// Biome to generate the dungeon.
/// </summary>
[DataField("biome", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<BiomePrototype>))]
public string Biome = string.Empty;
/// <summary>
/// Player-friendly description for the console.
/// </summary>
[DataField("desc")]
public string Description = string.Empty;
[DataField("difficultyRating")]
public DifficultyRating DifficultyRating = DifficultyRating.Minor;
// TODO: Make these modifiers but also add difficulty modifiers.
[DataField("light")]
public Color Light = Color.Black;
[DataField("temperature")]
public float Temperature = 293.15f;
[DataField("expedition", required: true)]
public ISalvageMission Mission = default!;
[DataField("minDuration")]
public TimeSpan MinDuration = TimeSpan.FromSeconds(9 * 60);
[DataField("maxDuration")]
public TimeSpan MaxDuration = TimeSpan.FromSeconds(12 * 60);
/// <summary>
/// Available factions for selection for this mission prototype.
/// </summary>
[DataField("factions", customTypeSerializer:typeof(PrototypeIdListSerializer<SalvageFactionPrototype>))]
public List<string> Factions = new();
[DataField("dungeonConfig", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<DungeonConfigPrototype>))]
public string DungeonConfigPrototype = string.Empty;
[DataField("reward", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
public string Reward = string.Empty;
/// <summary>
/// Possible loot prototypes available for this expedition.
/// This spawns during the mission and is not tied to completion.
/// </summary>
[DataField("loot", customTypeSerializer: typeof(PrototypeIdListSerializer<WeightedRandomPrototype>))]
public List<string> Loots = new();
[DataField("dungeonPosition")]
public Vector2i DungeonPosition = new(80, -25);
}
[Serializable, NetSerializable]
public enum DifficultyRating : byte
{
None,
Minor,
Moderate,
Hazardous,
Extreme,
}

View File

@@ -1,7 +1,8 @@
using Content.Shared.Salvage.Expeditions;
using Content.Shared.Salvage.Expeditions.Modifiers;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Salvage; namespace Content.Shared.Salvage;
@@ -10,13 +11,15 @@ public sealed class SalvageExpeditionConsoleState : BoundUserInterfaceState
{ {
public TimeSpan NextOffer; public TimeSpan NextOffer;
public bool Claimed; public bool Claimed;
public bool Cooldown;
public ushort ActiveMission; public ushort ActiveMission;
public List<SalvageMission> Missions; public List<SalvageMissionParams> Missions;
public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, ushort activeMission, List<SalvageMission> missions) public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, bool cooldown, ushort activeMission, List<SalvageMissionParams> missions)
{ {
NextOffer = nextOffer; NextOffer = nextOffer;
Claimed = claimed; Claimed = claimed;
Cooldown = cooldown;
ActiveMission = activeMission; ActiveMission = activeMission;
Missions = missions; Missions = missions;
} }
@@ -49,6 +52,12 @@ public sealed class SalvageExpeditionDataComponent : Component
[ViewVariables] [ViewVariables]
public bool Claimed => ActiveMission != 0; public bool Claimed => ActiveMission != 0;
/// <summary>
/// Are we actively cooling down from the last salvage mission.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
public bool Cooldown = false;
/// <summary> /// <summary>
/// Nexy time salvage missions are offered. /// Nexy time salvage missions are offered.
/// </summary> /// </summary>
@@ -56,7 +65,7 @@ public sealed class SalvageExpeditionDataComponent : Component
public TimeSpan NextOffer; public TimeSpan NextOffer;
[ViewVariables] [ViewVariables]
public readonly Dictionary<ushort, SalvageMission> Missions = new(); public readonly Dictionary<ushort, SalvageMissionParams> Missions = new();
[ViewVariables] public ushort ActiveMission; [ViewVariables] public ushort ActiveMission;
@@ -64,24 +73,84 @@ public sealed class SalvageExpeditionDataComponent : Component
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed record SalvageMission public sealed record SalvageMissionParams
{ {
[ViewVariables] [ViewVariables]
public ushort Index; public ushort Index;
[ViewVariables(VVAccess.ReadWrite), DataField("config", required: true, customTypeSerializer:typeof(SalvageExpeditionPrototype))] [ViewVariables(VVAccess.ReadWrite)]
public string Config = default!; public SalvageMissionType MissionType;
[ViewVariables] public TimeSpan Duration; [ViewVariables(VVAccess.ReadWrite)] public int Seed;
[ViewVariables] public int Seed; /// <summary>
/// Base difficulty for this mission.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] public DifficultyRating Difficulty;
} }
[Serializable, NetSerializable] /// <summary>
public enum SalvageEnvironment : byte /// Created from <see cref="SalvageMissionParams"/>. Only needed for data the client also needs for mission
/// display.
/// </summary>
public sealed record SalvageMission(
int Seed,
DifficultyRating Difficulty,
string Dungeon,
string Faction,
SalvageMissionType Mission,
string Biome,
Color? Color,
TimeSpan Duration,
Dictionary<string, int> Loot,
List<string> Modifiers)
{ {
Invalid = 0, /// <summary>
Caves, /// Seed used for the mission.
/// </summary>
public readonly int Seed = Seed;
/// <summary>
/// Difficulty rating.
/// </summary>
public DifficultyRating Difficulty = Difficulty;
/// <summary>
/// <see cref="SalvageDungeonMod"/> to be used.
/// </summary>
public readonly string Dungeon = Dungeon;
/// <summary>
/// <see cref="SalvageFactionPrototype"/> to be used.
/// </summary>
public readonly string Faction = Faction;
/// <summary>
/// Underlying mission params that generated this.
/// </summary>
public readonly SalvageMissionType Mission = Mission;
/// <summary>
/// Biome to be used for the mission.
/// </summary>
public readonly string Biome = Biome;
/// <summary>
/// Lighting color to be used (AKA outdoor lighting).
/// </summary>
public readonly Color? Color = Color;
/// <summary>
/// Mission duration.
/// </summary>
public TimeSpan Duration = Duration;
public Dictionary<string, int> Loot = Loot;
/// <summary>
/// Modifiers (outside of the above) applied to the mission.
/// </summary>
public List<string> Modifiers = Modifiers;
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]

View File

@@ -1,77 +1,235 @@
using System.Linq;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.Procedural.Loot; using Content.Shared.Procedural.Loot;
using Content.Shared.Procedural.Rewards;
using Content.Shared.Random; using Content.Shared.Random;
using Content.Shared.Random.Helpers; 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.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Salvage; namespace Content.Shared.Salvage;
public abstract class SharedSalvageSystem : EntitySystem public abstract class SharedSalvageSystem : EntitySystem
{ {
public static readonly TimeSpan MissionCooldown = TimeSpan.FromMinutes(5); [Dependency] private readonly ILocalizationManager _loc = default!;
public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(10); [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. // Hardcoded in coooooz it's dynamic based on difficulty and I'm lazy.
switch (difficulty) switch (mission.Mission)
{
case SalvageMissionType.Mining:
// Taxation: , ("tax", $"{GetMiningTax(mission.Difficulty) * 100f:0}")
return Loc.GetString("salvage-expedition-desc-mining");
case SalvageMissionType.Destruction:
var proto = _proto.Index<SalvageFactionPrototype>(mission.Faction).Configs["DefenseStructure"];
return Loc.GetString("salvage-expedition-desc-structure",
("count", GetStructureCount(mission.Difficulty)),
("structure", _loc.GetEntityData(proto).Name));
default:
throw new NotImplementedException();
}
}
public float GetMiningTax(DifficultyRating baseRating)
{
return 0.6f + (int) baseRating * 0.05f;
}
/// <summary>
/// Gets the amount of structures to destroy.
/// </summary>
public int GetStructureCount(DifficultyRating baseRating)
{
return 1 + (int) baseRating * 2;
}
#endregion
public int GetDifficulty(DifficultyRating rating)
{
switch (rating)
{ {
case DifficultyRating.None: case DifficultyRating.None:
return 1f; return 1;
case DifficultyRating.Minor: case DifficultyRating.Minor:
return 1.5f; return 2;
case DifficultyRating.Moderate: case DifficultyRating.Moderate:
return 3f; return 4;
case DifficultyRating.Hazardous: case DifficultyRating.Hazardous:
return 6f; return 6;
case DifficultyRating.Extreme: case DifficultyRating.Extreme:
return 10f; return 8;
default: default:
throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null); throw new ArgumentOutOfRangeException(nameof(rating), rating, null);
} }
} }
/// <summary>
/// How many groups of mobs to spawn for a mission.
/// </summary>
public float GetSpawnCount(DifficultyRating difficulty)
{
return (int) difficulty * 2;
}
public static string GetFTLName(DatasetPrototype dataset, int seed) public static string GetFTLName(DatasetPrototype dataset, int seed)
{ {
var random = new System.Random(seed); var random = new System.Random(seed);
return $"{dataset.Values[random.Next(dataset.Values.Count)]}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}"; return $"{dataset.Values[random.Next(dataset.Values.Count)]}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
} }
public static string GetFaction(List<string> factions, int seed) public SalvageMission GetMission(SalvageMissionType config, DifficultyRating difficulty, int seed)
{ {
var adjustedSeed = new System.Random(seed + 1); // This is on shared to ensure the client display for missions and what the server generates are consistent
return factions[adjustedSeed.Next(factions.Count)]; var rating = (float) GetDifficulty(difficulty);
// Don't want easy missions to have any negative modifiers but also want
// easy to be a 1 for difficulty.
rating -= 1f;
var rand = new System.Random(seed);
var faction = GetMod<SalvageFactionPrototype>(rand, ref rating);
var biome = GetMod<SalvageBiomeMod>(rand, ref rating);
var dungeon = GetDungeon(biome.ID, rand, ref rating);
var mods = new List<string>();
SalvageLightMod? light = null;
if (biome.BiomePrototype != null)
{
light = GetLight(biome.ID, rand, ref rating);
mods.Add(light.Description);
} }
public static IEnumerable<SalvageLootPrototype> GetLoot(List<string> loots, int seed, IPrototypeManager protoManager) var time = GetMod<SalvageTimeMod>(rand, ref rating);
// Round the duration to nearest 15 seconds.
var exactDuration = time.MinDuration + (time.MaxDuration - time.MinDuration) * rand.NextFloat();
exactDuration = MathF.Round(exactDuration / 15f) * 15f;
var duration = TimeSpan.FromSeconds(exactDuration);
if (time.ID != "StandardTime")
{ {
mods.Add(time.Description);
}
var loots = GetLoot(config, _proto.EnumeratePrototypes<SalvageLootPrototype>().ToList(), GetDifficulty(difficulty), seed);
return new SalvageMission(seed, difficulty, dungeon.ID, faction.ID, config, biome.ID, light?.Color, duration, loots, mods);
}
public SalvageDungeonMod GetDungeon(string biome, System.Random rand, ref float rating)
{
var mods = _proto.EnumeratePrototypes<SalvageDungeonMod>().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<SalvageLightMod>().ToList();
mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
rand.Shuffle(mods);
foreach (var mod in mods)
{
if (mod.Biomes?.Contains(biome) == false || mod.Cost > rating)
continue;
rating -= mod.Cost;
return mod;
}
throw new InvalidOperationException();
}
public T GetMod<T>(System.Random rand, ref float rating) where T : class, IPrototype, ISalvageMod
{
var mods = _proto.EnumeratePrototypes<T>().ToList();
mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
rand.Shuffle(mods);
foreach (var mod in mods)
{
if (mod.Cost > rating)
continue;
rating -= mod.Cost;
return mod;
}
throw new InvalidOperationException();
}
private Dictionary<string, int> GetLoot(SalvageMissionType mission, List<SalvageLootPrototype> loots, int count, int seed)
{
var results = new Dictionary<string, int>();
var adjustedSeed = new System.Random(seed + 2); 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]; adjustedSeed.Shuffle(loots);
var a = protoManager.Index<WeightedRandomPrototype>(loot);
var lootConfig = a.Pick(adjustedSeed);
yield return protoManager.Index<SalvageLootPrototype>(lootConfig);
}
}
public static ISalvageReward GetReward(WeightedRandomPrototype proto, int seed, IPrototypeManager protoManager) foreach (var loot in loots)
{ {
var adjustedSeed = new System.Random(seed + 3); if (loot.Blacklist.Contains(mission))
var rewardProto = proto.Pick(adjustedSeed); continue;
return protoManager.Index<SalvageRewardPrototype>(rewardProto).Reward;
var weh = results.GetOrNew(loot.ID);
weh++;
results[loot.ID] = weh;
break;
}
} }
#region Structure return results;
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
{
/// <summary>
/// No dungeon, just ore loot and random mob spawns.
/// </summary>
Mining,
/// <summary>
/// Destroy the specified structures in a dungeon.
/// </summary>
Destruction,
}
[Serializable, NetSerializable]
public enum DifficultyRating : byte
{
None,
Minor,
Moderate,
Hazardous,
Extreme,
} }

View File

@@ -0,0 +1,6 @@
cmd-biome_clear-desc = Clears a biome entirely
cmd-biome_clear-help = biome_clear <biomecomponent>
cmd-biome_addlayer-desc = Adds another biome layer
cmd-biome_addlayer-help = biome_addlayer <mapid> <biometemplate> [seed offset]
cmd-biome_addmarkerlayer-desc = Adds another biome marker layer
cmd-biome_addmarkerlayer-help = biome_addmarkerlayer <mapid> <biomemarkerlayer>

View File

@@ -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}.

View File

@@ -1089,12 +1089,6 @@ entities:
- pos: 0.5,0.5 - pos: 0.5,0.5
parent: 0 parent: 0
type: Transform type: Transform
- uid: 85
type: PaperWrittenSalvageLoreMedium1PlasmaTrap
components:
- pos: 0.48327154,0.5698495
parent: 0
type: Transform
- uid: 86 - uid: 86
type: ClothingEyesGlassesMeson type: ClothingEyesGlassesMeson
components: components:

View File

@@ -568,12 +568,6 @@ entities:
- pos: -3.5,0.5 - pos: -3.5,0.5
parent: 0 parent: 0
type: Transform type: Transform
- uid: 55
type: SalvageLorePaperGamingSpawner
components:
- pos: -1.5,-2.5
parent: 0
type: Transform
- uid: 56 - uid: 56
type: SalvageMobSpawner75 type: SalvageMobSpawner75
components: components:

View File

@@ -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
# ----

View File

@@ -89,6 +89,17 @@
- type: ComputerBoard - type: ComputerBoard
prototype: ComputerCargoShuttle 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 - type: entity
parent: BaseComputerCircuitboard parent: BaseComputerCircuitboard
id: CargoShuttleConsoleCircuitboard id: CargoShuttleConsoleCircuitboard

View File

@@ -717,6 +717,49 @@
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: StrongMetallic 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 - type: entity
parent: BaseComputer parent: BaseComputer
id: ComputerSurveillanceCameraMonitor id: ComputerSurveillanceCameraMonitor

View File

@@ -1,7 +1,6 @@
- type: entity - type: entity
id: XenoWardingTower id: XenoWardingTower
name: Xeno warding tower name: Xeno warding tower
description: a
placement: placement:
mode: SnapgridCenter mode: SnapgridCenter
snap: snap:
@@ -16,6 +15,7 @@
Heat: Heat:
collection: collection:
MeatLaserImpact MeatLaserImpact
- type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: Sprite - type: Sprite
netsync: false netsync: false

View File

@@ -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

View File

@@ -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

View File

@@ -1,8 +1,7 @@
# Desert # Desert
# TODO: Water in desert # TODO: Water in desert
- type: biome - type: biomeTemplate
id: LowDesert id: LowDesert
desc: Desert
layers: layers:
- !type:BiomeEntityLayer - !type:BiomeEntityLayer
threshold: 0.95 threshold: 0.95
@@ -33,6 +32,8 @@
- FloorLowDesert - FloorLowDesert
entities: entities:
- AsteroidRock - AsteroidRock
- !type:BiomeDummyLayer
id: Loot
# Fill layer # Fill layer
- !type:BiomeTileLayer - !type:BiomeTileLayer
threshold: -1 threshold: -1
@@ -48,9 +49,8 @@
frequency: 0.1 frequency: 0.1
# Grass # Grass
- type: biome - type: biomeTemplate
id: Grasslands id: Grasslands
desc: Grasslands
layers: layers:
# Sparse vegetation # Sparse vegetation
- !type:BiomeDecalLayer - !type:BiomeDecalLayer
@@ -142,6 +142,8 @@
cellularReturnType: Distance2 cellularReturnType: Distance2
entities: entities:
- WallRock - WallRock
- !type:BiomeDummyLayer
id: Loot
# Water # Water
- !type:BiomeEntityLayer - !type:BiomeEntityLayer
allowedTiles: allowedTiles:
@@ -195,9 +197,8 @@
cellularReturnType: Distance2 cellularReturnType: Distance2
# Lava # Lava
- type: biome - type: biomeTemplate
id: Lava id: Lava
desc: Lava
layers: layers:
- !type:BiomeEntityLayer - !type:BiomeEntityLayer
threshold: 0.9 threshold: 0.9
@@ -255,6 +256,8 @@
- FloorBasalt - FloorBasalt
entities: entities:
- FloorLavaEntity - FloorLavaEntity
- !type:BiomeDummyLayer
id: Loot
# Fill basalt # Fill basalt
- !type:BiomeTileLayer - !type:BiomeTileLayer
threshold: -1 threshold: -1
@@ -263,7 +266,7 @@
tile: FloorBasalt tile: FloorBasalt
# Snow # Snow
- type: biome - type: biomeTemplate
id: Snow # Similar to Grasslands... but snow id: Snow # Similar to Grasslands... but snow
layers: layers:
# Sparse vegetation # Sparse vegetation
@@ -353,6 +356,8 @@
- FloraTreeSnow04 - FloraTreeSnow04
- FloraTreeSnow05 - FloraTreeSnow05
- FloraTreeSnow06 - FloraTreeSnow06
- !type:BiomeDummyLayer
id: Loot
- !type:BiomeTileLayer - !type:BiomeTileLayer
threshold: -1.0 threshold: -1.0
tile: FloorSnow tile: FloorSnow
@@ -363,3 +368,27 @@
seed: 0 seed: 0
frequency: 0.02 frequency: 0.02
fractalType: None 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

View File

@@ -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

View File

@@ -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

View File

@@ -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