_viewBox.AddChild(viewWest);
_viewBox.AddChild(viewEast);
_summaryLabel.Text = selectedCharacter.Summary;
- EntitySystem.Get<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
+ _entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
}
}
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
- Title="{Loc 'Salvage expeditions'}"
+ Title="{Loc 'salvage-expedition-window-title'}"
MinSize="800 360">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="NextOfferLabel"
- Text="Next offer:"
+ Text="{Loc 'salvage-expedition-window-next'}"
Margin="5"></Label>
<ProgressBar Name="NextOfferBar"
HorizontalExpand="True"
+using System.Linq;
using Content.Client.Computer;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Parallax.Biomes;
-using Content.Shared.Procedural;
using Content.Shared.Procedural.Loot;
-using Content.Shared.Procedural.Rewards;
-using Content.Shared.Random;
-using Content.Shared.Random.Helpers;
using Content.Shared.Salvage;
-using Content.Shared.Salvage.Expeditions;
-using Content.Shared.Salvage.Expeditions.Structure;
+using Content.Shared.Salvage.Expeditions.Modifiers;
using Content.Shared.Shuttles.BUIStates;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
public event Action<ushort>? ClaimMission;
private bool _claimed;
+ private bool _cooldown;
private TimeSpan _nextOffer;
public SalvageExpeditionWindow()
public void UpdateState(SalvageExpeditionConsoleState state)
{
_claimed = state.Claimed;
+ _cooldown = state.Cooldown;
_nextOffer = state.NextOffer;
Container.DisposeAllChildren();
for (var i = 0; i < state.Missions.Count; i++)
{
- // TODO: Make this XAML
- var mission = state.Missions[i];
- var config = _prototype.Index<SalvageExpeditionPrototype>(mission.Config);
- var dungeonConfig = _prototype.Index<DungeonConfigPrototype>(config.DungeonConfigPrototype);
- var faction = SharedSalvageSystem.GetFaction(config.Factions, mission.Seed);
- var factionConfig = _prototype.Index<SalvageFactionPrototype>(faction);
-
- // If we ever need this on server then move it
- var missionDesc = string.Empty;
- var missionDetails = string.Empty;
-
- switch (config.Mission)
- {
- case SalvageStructure structure:
- var structureConfig = (SalvageStructureFaction) factionConfig.Configs[mission.Config];
- missionDesc = "Demolition";
- // TODO:
- missionDetails = $"Destroy {SharedSalvageSystem.GetStructureCount(structure, mission.Seed)} {_prototype.Index<EntityPrototype>(structureConfig.Spawn).Name} structures.";
- break;
- default:
- throw new NotImplementedException();
- }
+ var missionParams = state.Missions[i];
+ var config = missionParams.MissionType;
+ var mission = _salvage.GetMission(missionParams.MissionType, missionParams.Difficulty, missionParams.Seed);
- // Mission
+ // Mission title
var missionStripe = new StripeBack()
{
Margin = new Thickness(0f, -5f, 0f, 0f)
missionStripe.AddChild(new Label()
{
- Text = missionDesc,
+ Text = Loc.GetString($"salvage-expedition-type-{config.ToString()}"),
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(0f, 5f, 0f, 5f),
});
// Details
lBox.AddChild(new Label()
{
- Text = $"Difficulty:"
+ Text = Loc.GetString("salvage-expedition-window-difficulty")
});
- var difficultyColor = StyleNano.NanoGold;
+ Color difficultyColor;
- switch (config.DifficultyRating)
+ switch (missionParams.Difficulty)
{
case DifficultyRating.None:
- difficultyColor = StyleNano.ButtonColorDefault;
+ difficultyColor = Color.FromHex("#52B4E996");
break;
case DifficultyRating.Minor:
- difficultyColor = StyleNano.GoodGreenFore;
+ difficultyColor = Color.FromHex("#9FED5896");
break;
case DifficultyRating.Moderate:
- difficultyColor = StyleNano.ConcerningOrangeFore;
+ difficultyColor = Color.FromHex("#EFB34196");
+ break;
+ case DifficultyRating.Hazardous:
+ difficultyColor = Color.FromHex("#DE3A3A96");
+ break;
+ case DifficultyRating.Extreme:
+ difficultyColor = Color.FromHex("#D381C996");
break;
default:
throw new ArgumentOutOfRangeException();
lBox.AddChild(new Label
{
- Text = config.DifficultyRating.ToString(),
+ Text = Loc.GetString($"salvage-expedition-difficulty-{missionParams.Difficulty.ToString()}"),
FontColorOverride = difficultyColor,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
});
// Details
+ var details = _salvage.GetMissionDescription(mission);
+
lBox.AddChild(new Label
{
- Text = $"Details:"
+ Text = Loc.GetString("salvage-expedition-window-details")
});
lBox.AddChild(new Label
{
- Text = missionDetails,
+ Text = details,
FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
// Details
lBox.AddChild(new Label
{
- Text = $"Hostiles:"
+ Text = Loc.GetString("salvage-expedition-window-hostiles")
});
+ var faction = mission.Faction;
+
lBox.AddChild(new Label
{
Text = faction,
// Duration
lBox.AddChild(new Label
{
- Text = $"Duration:"
+ Text = Loc.GetString("salvage-expedition-window-duration")
});
lBox.AddChild(new Label
// Biome
lBox.AddChild(new Label
{
- Text = "Biome:"
+ Text = Loc.GetString("salvage-expedition-window-biome")
});
- lBox.AddChild(new Label
- {
- Text = _prototype.Index<BiomePrototype>(config.Biome).Description,
- FontColorOverride = StyleNano.NanoGold,
- HorizontalAlignment = HAlignment.Left,
- Margin = new Thickness(0f, 0f, 0f, 5f),
- });
-
- // Environment
- lBox.AddChild(new Label
- {
- Text = "Environment:"
- });
+ var biome = mission.Biome;
lBox.AddChild(new Label
{
- Text = config.Description,
+ Text = Loc.GetString(_prototype.Index<SalvageBiomeMod>(biome).ID),
FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
});
// Modifiers
- // TODO
-
- // Rewards
- lBox.AddChild(new Label()
+ lBox.AddChild(new Label
{
- Text = $"Reward:"
+ Text = Loc.GetString("salvage-expedition-window-modifiers")
});
- var salvageReward = SharedSalvageSystem.GetReward(_prototype.Index<WeightedRandomPrototype>(config.Reward), mission.Seed, _prototype);
- var difficulty = config.DifficultyRating;
- var rewardDesc = string.Empty;
+ var mods = mission.Modifiers;
- switch (salvageReward)
- {
- case BankReward bank:
- rewardDesc = $"Bank payment of {(int) (bank.Amount * SharedSalvageSystem.GetDifficultyModifier(difficulty))}";
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
-
- lBox.AddChild(new Label()
+ lBox.AddChild(new Label
{
- Text = rewardDesc,
- FontColorOverride = StyleNano.GoodGreenFore,
+ Text = string.Join("\n", mods.Select(o => "- " + o)).TrimEnd(),
+ FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
});
-
lBox.AddChild(new Label()
{
- Text = $"Materials:"
+ Text = Loc.GetString("salvage-expedition-window-loot")
});
- if (config.Loots.Count == 0)
+ if (mission.Loot.Count == 0)
{
lBox.AddChild(new Label()
{
- Text = "N/A",
+ Text = Loc.GetString("salvage-expedition-window-none"),
FontColorOverride = StyleNano.ConcerningOrangeFore,
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
}
else
{
- foreach (var lootProto in SharedSalvageSystem.GetLoot(config.Loots, mission.Seed, _prototype))
+ lBox.AddChild(new Label()
{
- lBox.AddChild(new Label()
- {
- Text = lootProto.Description,
- FontColorOverride = StyleNano.ConcerningOrangeFore,
- HorizontalAlignment = HAlignment.Left,
- Margin = new Thickness(0f, 0f, 0f, 5f),
- });
- }
+ Text = string.Join("\n", mission.Loot.Select(o => "- " + _prototype.Index<SalvageLootPrototype>(o.Key).Description + (o.Value > 1 ? $" x {o.Value}" : ""))).TrimEnd(),
+ FontColorOverride = StyleNano.ConcerningOrangeFore,
+ HorizontalAlignment = HAlignment.Left,
+ Margin = new Thickness(0f, 0f, 0f, 5f),
+ });
}
// Claim
var claimButton = new Button()
{
HorizontalExpand = true,
- Pressed = state.ActiveMission == mission.Index,
+ VerticalAlignment = VAlignment.Bottom,
+ Pressed = state.ActiveMission == missionParams.Index,
ToggleMode = true,
- Disabled = state.Claimed,
+ Disabled = state.Claimed || state.Cooldown,
};
claimButton.Label.Margin = new Thickness(0f, 5f);
claimButton.OnPressed += args =>
{
- ClaimMission?.Invoke(mission.Index);
+ ClaimMission?.Invoke(missionParams.Index);
};
- if (state.ActiveMission == mission.Index)
+ if (state.ActiveMission == missionParams.Index)
{
- claimButton.Text = "Claimed";
+ claimButton.Text = Loc.GetString("salvage-expedition-window-claimed");
claimButton.AddStyleClass(StyleBase.ButtonCaution);
}
else
{
- claimButton.Text = "Claim";
+ claimButton.Text = Loc.GetString("salvage-expedition-window-claim");
}
- // TODO: Fix this copypaste bullshit
-
- var box = new PanelContainer()
+ var box = new PanelContainer
{
PanelOverride = new StyleBoxFlat(new Color(30, 30, 34)),
HorizontalExpand = true,
Margin = new Thickness(5f, 0f),
Children =
{
- new BoxContainer()
+ new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
missionStripe,
lBox,
+ new Control() {VerticalExpand = true},
claimButton,
},
Margin = new Thickness(5f, 5f)
if (_claimed)
{
NextOfferBar.Value = 0f;
- NextOfferText.Text = "N/A";
+ NextOfferText.Text = "00:00";
return;
}
}
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}";
}
}
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
using Content.Shared.Kitchen;
-using Content.Shared.Module;
namespace Content.Server.IoC
{
return;
}
- if (!_protoManager.HasIndex<BiomePrototype>(args[1]))
+ if (!_protoManager.TryIndex<BiomeTemplatePrototype>(args[1], out var biomeTemplate))
{
shell.WriteError(Loc.GetString("cmd-planet-map-prototype", ("prototype", args[1])));
return;
MetaDataComponent? metadata = null;
var biome = _entManager.EnsureComponent<BiomeComponent>(mapUid);
- _entManager.System<BiomeSystem>().SetPrototype(biome, args[1]);
- _entManager.System<BiomeSystem>().SetSeed(biome, _random.Next());
+ var biomeSystem = _entManager.System<BiomeSystem>();
+ biomeSystem.SetSeed(biome, _random.Next());
+ biomeSystem.SetTemplate(biome, biomeTemplate);
_entManager.Dirty(biome);
var gravity = _entManager.EnsureComponent<GravityComponent>(mapUid);
if (args.Length == 2)
{
- var options = _protoManager.EnumeratePrototypes<BiomePrototype>()
+ var options = _protoManager.EnumeratePrototypes<BiomeTemplatePrototype>()
.Select(o => new CompletionOption(o.ID, "Biome"));
return CompletionResult.FromOptions(options);
}
public void UpdateNPC(ref int count, int maxUpdates, float frameTime)
{
_planQueue.Process();
+ var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
- foreach (var (_, comp) in EntityQuery<ActiveNPCComponent, HTNComponent>())
+ while(query.MoveNext(out var uid, out _, out var comp))
{
// If we're over our max count or it's not MapInit then ignore the NPC.
if (count >= maxUpdates)
{
if (comp.PlanningJob.Exception != null)
{
- _sawmill.Fatal($"Received exception on planning job for {comp.Owner}!");
- _npc.SleepNPC(comp.Owner);
+ _sawmill.Fatal($"Received exception on planning job for {uid}!");
+ _npc.SleepNPC(uid);
var exc = comp.PlanningJob.Exception;
- RemComp<HTNComponent>(comp.Owner);
+ RemComp<HTNComponent>(uid);
throw exc;
}
RaiseNetworkEvent(new HTNMessage()
{
- Uid = comp.Owner,
+ Uid = uid,
Text = text.ToString(),
}, session.ConnectedClient);
}
--- /dev/null
+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;
+ }
+}
using Content.Server.Decals;
+using Content.Server.Shuttles.Events;
using Content.Shared.Decals;
using Content.Shared.Parallax.Biomes;
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
+using Content.Shared.Parallax.Biomes.Points;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
+using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Noise;
using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Shared.Utility;
namespace Content.Server.Parallax;
-public sealed class BiomeSystem : SharedBiomeSystem
+public sealed partial class BiomeSystem : SharedBiomeSystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly IConsoleHost _console = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DecalSystem _decals = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private readonly HashSet<EntityUid> _handledEntities = new();
private const float DefaultLoadRange = 16f;
private float _loadRange = DefaultLoadRange;
+
+ /// <summary>
+ /// Load area for chunks containing tiles, decals etc.
+ /// </summary>
private Box2 _loadArea = new(-DefaultLoadRange, -DefaultLoadRange, DefaultLoadRange, DefaultLoadRange);
/// <summary>
/// </summary>
private readonly Dictionary<BiomeComponent, HashSet<Vector2i>> _activeChunks = new();
+ private readonly Dictionary<BiomeComponent,
+ Dictionary<string, HashSet<Vector2i>>> _markerChunks = new();
+
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BiomeComponent, ComponentStartup>(OnBiomeStartup);
SubscribeLocalEvent<BiomeComponent, MapInitEvent>(OnBiomeMapInit);
+ SubscribeLocalEvent<FTLStartedEvent>(OnFTLStarted);
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, SetLoadRange, true);
+ InitializeCommands();
+ _proto.PrototypesReloaded += ProtoReload;
}
public override void Shutdown()
{
base.Shutdown();
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, SetLoadRange);
+ _proto.PrototypesReloaded -= ProtoReload;
+ }
+
+ private void ProtoReload(PrototypesReloadedEventArgs obj)
+ {
+ if (!obj.ByType.TryGetValue(typeof(BiomeTemplatePrototype), out var reloads))
+ return;
+
+ var query = AllEntityQuery<BiomeComponent>();
+
+ while (query.MoveNext(out var biome))
+ {
+ if (biome.Template == null || !reloads.Modified.TryGetValue(biome.Template, out var proto))
+ continue;
+
+ SetTemplate(biome, (BiomeTemplatePrototype) proto);
+ }
}
private void SetLoadRange(float obj)
SetSeed(component, _random.Next());
}
- public void SetPrototype(BiomeComponent component, string proto)
+ public void SetSeed(BiomeComponent component, int seed)
+ {
+ component.Seed = seed;
+ component.Noise.SetSeed(seed);
+ Dirty(component);
+ }
+
+ public void ClearTemplate(BiomeComponent component)
{
- if (component.BiomePrototype == proto)
+ component.Layers.Clear();
+ component.Template = null;
+ Dirty(component);
+ }
+
+ /// <summary>
+ /// Sets the <see cref="BiomeComponent.Template"/> and refreshes layers.
+ /// </summary>
+ public void SetTemplate(BiomeComponent component, BiomeTemplatePrototype template)
+ {
+ component.Layers.Clear();
+ component.Template = template.ID;
+
+ foreach (var layer in template.Layers)
+ {
+ component.Layers.Add(layer);
+ }
+
+ Dirty(component);
+ }
+
+ /// <summary>
+ /// Adds the specified layer at the specified marker if it exists.
+ /// </summary>
+ public void AddLayer(BiomeComponent component, string id, IBiomeLayer addedLayer, int seedOffset = 0)
+ {
+ for (var i = 0; i < component.Layers.Count; i++)
+ {
+ var layer = component.Layers[i];
+
+ if (layer is not BiomeDummyLayer dummy || dummy.ID != id)
+ continue;
+
+ addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset);
+ component.Layers.Insert(i, addedLayer);
+ break;
+ }
+
+ Dirty(component);
+ }
+
+ public void AddMarkerLayer(BiomeComponent component, string marker)
+ {
+ if (!_proto.HasIndex<BiomeMarkerLayerPrototype>(marker))
+ {
+ // TODO: Log when we get a sawmill
return;
+ }
- component.BiomePrototype = proto;
+ component.MarkerLayers.Add(marker);
Dirty(component);
}
- public void SetSeed(BiomeComponent component, int seed)
+ /// <summary>
+ /// Adds the specified template at the specified marker if it exists, withour overriding every layer.
+ /// </summary>
+ public void AddTemplate(BiomeComponent component, string id, BiomeTemplatePrototype template, int seedOffset = 0)
{
- component.Seed = seed;
- component.Noise.SetSeed(seed);
+ for (var i = 0; i < component.Layers.Count; i++)
+ {
+ var layer = component.Layers[i];
+
+ if (layer is not BiomeDummyLayer dummy || dummy.ID != id)
+ continue;
+
+ for (var j = template.Layers.Count - 1; j >= 0; j--)
+ {
+ var addedLayer = template.Layers[j];
+ addedLayer.Noise.SetSeed(addedLayer.Noise.GetSeed() + seedOffset);
+ component.Layers.Insert(i, addedLayer);
+ }
+
+ break;
+ }
+
Dirty(component);
}
+ private void OnFTLStarted(ref FTLStartedEvent ev)
+ {
+ var targetMap = ev.TargetCoordinates.ToMap(EntityManager, _transform);
+ var targetMapUid = _mapManager.GetMapEntityId(targetMap.MapId);
+
+ if (!TryComp<BiomeComponent>(targetMapUid, out var biome))
+ return;
+
+ var targetArea = new Box2(targetMap.Position - 64f, targetMap.Position + 64f);
+ Preload(targetMapUid, biome, targetArea);
+ }
+
+ /// <summary>
+ /// Preloads biome for the specified area.
+ /// </summary>
+ public void Preload(EntityUid uid, BiomeComponent component, Box2 area)
+ {
+ var markers = component.MarkerLayers;
+ var goobers = _markerChunks.GetOrNew(component);
+
+ foreach (var layer in markers)
+ {
+ var proto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+ var enumerator = new ChunkIndicesEnumerator(area, proto.Size);
+
+ while (enumerator.MoveNext(out var chunk))
+ {
+ var chunkOrigin = chunk * proto.Size;
+ var layerChunks = goobers.GetOrNew(proto.ID);
+ layerChunks.Add(chunkOrigin.Value);
+ }
+ }
+ }
+
public override void Update(float frameTime)
{
base.Update(frameTime);
while (biomes.MoveNext(out var biome))
{
_activeChunks.Add(biome, new HashSet<Vector2i>());
+ _markerChunks.GetOrNew(biome);
}
// Get chunks in range
_handledEntities.Add(pSession.AttachedEntity.Value) &&
biomeQuery.TryGetComponent(xform.MapUid, out var biome))
{
- AddChunksInRange(biome, _transform.GetWorldPosition(xform, xformQuery));
+ var worldPos = _transform.GetWorldPosition(xform, xformQuery);
+ AddChunksInRange(biome, worldPos);
+
+ foreach (var layer in biome.MarkerLayers)
+ {
+ var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+ AddMarkerChunksInRange(biome, worldPos, layerProto);
+ }
}
foreach (var viewer in pSession.ViewSubscriptions)
continue;
}
- AddChunksInRange(biome, _transform.GetWorldPosition(xform, xformQuery));
+ var worldPos = _transform.GetWorldPosition(xform, xformQuery);
+ AddChunksInRange(biome, worldPos);
+
+ foreach (var layer in biome.MarkerLayers)
+ {
+ var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+ AddMarkerChunksInRange(biome, worldPos, layerProto);
+ }
}
}
var loadBiomes = AllEntityQuery<BiomeComponent, MapGridComponent>();
- while (loadBiomes.MoveNext(out var biome, out var grid))
+ while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
{
var noise = biome.Noise;
- var gridUid = grid.Owner;
// Load new chunks
LoadChunks(biome, gridUid, grid, noise, xformQuery);
_handledEntities.Clear();
_activeChunks.Clear();
+ _markerChunks.Clear();
}
private void AddChunksInRange(BiomeComponent biome, Vector2 worldPos)
}
}
+ private void AddMarkerChunksInRange(BiomeComponent biome, Vector2 worldPos, IBiomeMarkerLayer layer)
+ {
+ // Offset the load area so it's centralised.
+ var loadArea = new Box2(0, 0, layer.Size, layer.Size);
+ var enumerator = new ChunkIndicesEnumerator(loadArea.Translated(worldPos - layer.Size / 2f), layer.Size);
+
+ while (enumerator.MoveNext(out var chunkOrigin))
+ {
+ var lay = _markerChunks[biome].GetOrNew(layer.ID);
+ lay.Add(chunkOrigin.Value * layer.Size);
+ }
+ }
+
+ #region Load
+
private void LoadChunks(
BiomeComponent component,
EntityUid gridUid,
FastNoiseLite noise,
EntityQuery<TransformComponent> xformQuery)
{
+ var markers = _markerChunks[component];
+ var loadedMarkers = component.LoadedMarkers;
+
+ foreach (var (layer, chunks) in markers)
+ {
+ foreach (var chunk in chunks)
+ {
+ if (loadedMarkers.TryGetValue(layer, out var mobChunks) && mobChunks.Contains(chunk))
+ continue;
+
+ var layerProto = _proto.Index<BiomeMarkerLayerPrototype>(layer);
+ var buffer = layerProto.Radius / 2f;
+ mobChunks ??= new HashSet<Vector2i>();
+ mobChunks.Add(chunk);
+ loadedMarkers[layer] = mobChunks;
+ var rand = new Random(noise.GetSeed() + chunk.X * 8 + chunk.Y);
+
+ // Load NOW
+ // TODO: Need poisson but crashes whenever I use moony's due to inputs or smth
+ var count = (int) ((layerProto.Size - buffer) * (layerProto.Size - buffer) / (layerProto.Radius * layerProto.Radius));
+
+ for (var i = 0; i < count; i++)
+ {
+ for (var j = 0; j < 5; j++)
+ {
+ var point = new Vector2(
+ chunk.X + buffer * rand.NextFloat() * (layerProto.Size - buffer),
+ chunk.Y + buffer * rand.NextFloat() * (layerProto.Size - buffer));
+
+ var coords = new EntityCoordinates(gridUid, point);
+ var tile = grid.LocalToTile(coords);
+
+ // Blocked spawn, try again.
+ if (grid.GetAnchoredEntitiesEnumerator(tile).MoveNext(out _))
+ continue;
+
+ for (var k = 0; k < layerProto.GroupCount; k++)
+ {
+ Spawn(layerProto.Prototype, new EntityCoordinates(gridUid, point));
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
var active = _activeChunks[component];
- var prototype = ProtoManager.Index<BiomePrototype>(component.BiomePrototype);
List<(Vector2i, Tile)>? tiles = null;
foreach (var chunk in active)
tiles ??= new List<(Vector2i, Tile)>(ChunkSize * ChunkSize);
// Load NOW!
- LoadChunk(component, gridUid, grid, chunk, noise, prototype, tiles, xformQuery);
+ LoadChunk(component, gridUid, grid, chunk, noise, tiles, xformQuery);
}
}
MapGridComponent grid,
Vector2i chunk,
FastNoiseLite noise,
- BiomePrototype prototype,
List<(Vector2i, Tile)> tiles,
EntityQuery<TransformComponent> xformQuery)
{
continue;
// Pass in null so we don't try to get the tileref.
- if (!TryGetBiomeTile(indices, prototype, noise, null, out var biomeTile) || biomeTile.Value == tileRef.Tile)
+ if (!TryGetBiomeTile(indices, component.Layers, noise, null, out var biomeTile) || biomeTile.Value == tileRef.Tile)
continue;
tiles.Add((indices, biomeTile.Value));
// Don't mess with anything that's potentially anchored.
var anchored = grid.GetAnchoredEntitiesEnumerator(indices);
- if (anchored.MoveNext(out _) || !TryGetEntity(indices, prototype, noise, grid, out var entPrototype))
+ if (anchored.MoveNext(out _) || !TryGetEntity(indices, component.Layers, noise, grid, out var entPrototype))
continue;
// TODO: Fix non-anchored ents spawning.
// At least for now unless we do lookups or smth, only work with anchoring.
if (xformQuery.TryGetComponent(ent, out var xform) && !xform.Anchored)
{
- _transform.AnchorEntity(ent, xform, grid, indices);
+ _transform.AnchorEntity(ent, xform, gridUid, grid, indices);
}
loadedEntities.Add(ent);
// Don't mess with anything that's potentially anchored.
var anchored = grid.GetAnchoredEntitiesEnumerator(indices);
- if (anchored.MoveNext(out _) || !TryGetDecals(indices, prototype, noise, grid, out var decals))
+ if (anchored.MoveNext(out _) || !TryGetDecals(indices, component.Layers, noise, grid, out var decals))
continue;
foreach (var decal in decals)
}
}
+ #endregion
+
+ #region Unload
+
private void UnloadChunks(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, FastNoiseLite noise)
{
var active = _activeChunks[component];
private void UnloadChunk(BiomeComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i chunk, FastNoiseLite noise, List<(Vector2i, Tile)> tiles)
{
// Reverse order to loading
- var prototype = ProtoManager.Index<BiomePrototype>(component.BiomePrototype);
component.ModifiedTiles.TryGetValue(chunk, out var modified);
modified ??= new HashSet<Vector2i>();
}
// If it's default data unload the tile.
- if (!TryGetBiomeTile(indices, prototype, noise, null, out var biomeTile) ||
+ if (!TryGetBiomeTile(indices, component.Layers, noise, null, out var biomeTile) ||
grid.TryGetTileRef(indices, out var tileRef) && tileRef.Tile != biomeTile.Value)
{
modified.Add(indices);
component.ModifiedTiles[chunk] = modified;
}
}
+
+ #endregion
}
}
var tiles = new List<(Vector2i, Tile)>();
- var dungeon = new Dungeon();
+ var dungeon = new Dungeon()
+ {
+ Position = _position
+ };
var availablePacks = new List<DungeonRoomPackPrototype>();
var chosenPacks = new DungeonRoomPackPrototype?[gen.RoomPacks.Count];
var packTransforms = new Matrix3[gen.RoomPacks.Count];
}
}
+ // Calculate center
+ var dungeonCenter = Vector2.Zero;
+
+ foreach (var room in dungeon.Rooms)
+ {
+ dungeonCenter += room.Center;
+ }
+
+ dungeon.Center = (Vector2i) (dungeonCenter / dungeon.Rooms.Count);
+
return dungeon;
}
}
var rooms = new List<DungeonRoom>(dungeon.Rooms);
var roomTiles = new List<Vector2i>();
var tileData = new Tile(_tileDefManager[gen.Tile].TileId);
- var count = gen.Count;
- while (count > 0 && rooms.Count > 0)
+ for (var i = 0; i < gen.Count; i++)
{
var roomIndex = random.Next(rooms.Count);
var room = rooms[roomIndex];
- rooms.RemoveAt(roomIndex);
// Move out 3 tiles in a direction away from center of the room
// If none of those intersect another tile it's probably external
foreach (var tile in roomTiles)
{
- // Check the interior node is at least accessible?
- // Can't do anchored because it might be a locker or something.
- // TODO: Better collision mask check
- if (_lookup.GetEntitiesIntersecting(gridUid, tile, LookupFlags.Dynamic | LookupFlags.Static).Any())
- continue;
-
var direction = (tile - room.Center).ToAngle().GetCardinalDir().ToAngle().ToVec();
var isValid = true;
_entManager.SpawnEntity(ent, gridCoords);
}
- count--;
-
// Clear out any biome tiles nearby to avoid blocking it
foreach (var nearTile in grid.GetTilesIntersecting(new Circle(gridCoords.Position, 1.5f), false))
{
private readonly DungeonConfigPrototype _gen;
private readonly int _seed;
- private readonly Vector2 _position;
+ private readonly Vector2i _position;
private readonly MapGridComponent _grid;
private readonly EntityUid _gridUid;
MapGridComponent grid,
EntityUid gridUid,
int seed,
- Vector2 position,
+ Vector2i position,
CancellationToken cancellation = default) : base(maxTime, cancellation)
{
_sawmill = sawmill;
return;
}
- var position = new Vector2(posX, posY);
+ var position = new Vector2i(posX, posY);
var dungeonUid = _mapManager.GetMapEntityId(mapId);
if (!TryComp<MapGridComponent>(dungeonUid, out var dungeonGrid))
public void GenerateDungeon(DungeonConfigPrototype gen,
EntityUid gridUid,
MapGridComponent grid,
- Vector2 position,
+ Vector2i position,
int seed)
{
var cancelToken = new CancellationTokenSource();
DungeonConfigPrototype gen,
EntityUid gridUid,
MapGridComponent grid,
- Vector2 position,
+ Vector2i position,
int seed)
{
var cancelToken = new CancellationTokenSource();
--- /dev/null
+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,
+}
--- /dev/null
+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();
+}
--- /dev/null
+namespace Content.Server.Salvage.Expeditions.Structure;
+
+/// <summary>
+/// Mission objective for salvage expeditions.
+/// </summary>
+[RegisterComponent, Access(typeof(SalvageSystem))]
+public sealed class SalvageStructureComponent : Component
+{
+
+}
--- /dev/null
+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();
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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"));
+ }
+}
--- /dev/null
+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)));
+ }
+ }
+ }
+}
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
-using Content.Server.Cargo.Systems;
-using Content.Server.NPC.Pathfinding;
+using Content.Server.Chat.Managers;
+using Content.Server.Chat.Systems;
using Content.Server.Parallax;
using Content.Server.Procedural;
+using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Robust.Shared.Timing;
{
public sealed partial class SalvageSystem : SharedSalvageSystem
{
+ [Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly BiomeSystem _biome = default!;
- [Dependency] private readonly CargoSystem _cargo = default!;
[Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
- [Dependency] private readonly PathfindingSystem _pathfinding = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly ShuttleSystem _shuttle = default!;
+ [Dependency] private readonly ShuttleConsoleSystem _shuttleConsoles = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
// Can't use RoundRestartCleanupEvent, I need to clean up before the grid, and components are gone to prevent the announcements
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRoundEnd);
+
+ InitializeExpeditions();
+ InitializeRunner();
}
private void OnRoundEnd(GameRunLevelChangedEvent ev)
state.ActiveMagnets.Remove(magnet);
}
}
+
+ UpdateExpeditions();
+ UpdateRunner();
}
}
--- /dev/null
+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
+}
/// Raised when <see cref="ShuttleSystem.FasterThanLight"/> has completed FTL Travel.
/// </summary>
[ByRefEvent]
-public readonly record struct FTLCompletedEvent;
+public readonly record struct FTLCompletedEvent(EntityUid Entity, EntityUid MapUid);
--- /dev/null
+namespace Content.Server.Shuttles.Events;
+
+/// <summary>
+/// Raised by a shuttle when it has requested an FTL.
+/// </summary>
+[ByRefEvent]
+public record struct FTLRequestEvent(EntityUid MapUid);
/// Raised when a shuttle has moved to FTL space.
/// </summary>
[ByRefEvent]
-public readonly record struct FTLStartedEvent(EntityUid? FromMapUid, Matrix3 FTLFrom, Angle FromRotation);
+public readonly record struct FTLStartedEvent(EntityUid Entity, EntityCoordinates TargetCoordinates, EntityUid? FromMapUid, Matrix3 FTLFrom, Angle FromRotation);
hyperspace.Dock = false;
hyperspace.PriorityTag = priorityTag;
_console.RefreshShuttleConsoles();
+ var ev = new FTLRequestEvent(_mapManager.GetMapEntityId(coordinates.ToMap(EntityManager, _transform).MapId));
+ RaiseLocalEvent(shuttleUid, ref ev, true);
}
/// <summary>
SetDockBolts(uid, true);
_console.RefreshShuttleConsoles(uid);
- var ev = new FTLStartedEvent(fromMapUid, fromMatrix, fromRotation);
- RaiseLocalEvent(uid, ref ev);
+ var target = comp.TargetUid != null ? new EntityCoordinates(comp.TargetUid.Value, Vector2.Zero) : comp.TargetCoordinates;
+
+ var ev = new FTLStartedEvent(uid, target, fromMapUid, fromMatrix, fromRotation);
+ RaiseLocalEvent(uid, ref ev, true);
if (comp.TravelSound != null)
{
comp.Accumulator += FTLCooldown;
_console.RefreshShuttleConsoles(uid);
_mapManager.SetMapPaused(mapId, false);
- var ftlEvent = new FTLCompletedEvent();
+ var ftlEvent = new FTLCompletedEvent(uid, _mapManager.GetMapEntityId(mapId));
RaiseLocalEvent(uid, ref ftlEvent, true);
break;
case FTLState.Cooldown:
public bool TryFTLProximity(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, TransformComponent? xform = null, TransformComponent? targetXform = null)
{
if (!Resolve(targetUid, ref targetXform) ||
+ targetXform.GridUid == null ||
targetXform.MapUid == null ||
!targetXform.MapUid.Value.IsValid() ||
!Resolve(shuttleUid, ref xform))
spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
}
+ // TODO: This is pretty crude for multiple landings.
+ if (nearbyGrids.Count > 1 || !HasComp<MapComponent>(targetXform.GridUid.Value))
+ {
+ var minRadius = (MathF.Max(targetAABB.Width, targetAABB.Height) + MathF.Max(shuttleAABB.Width, shuttleAABB.Height)) / 2f;
+ spawnPos = targetAABB.Center + _random.NextVector2(minRadius, minRadius + 64f);
+ }
+ else if (shuttleBody != null)
+ {
+ var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform, xformQuery);
+ var transform = new Transform(targetPos, targetRot);
+ spawnPos = Robust.Shared.Physics.Transform.Mul(transform, -shuttleBody.LocalCenter);
+ }
+ else
+ {
+ spawnPos = _transform.GetWorldPosition(targetXform, xformQuery);
+ }
+
xform.Coordinates = new EntityCoordinates(targetXform.MapUid.Value, spawnPos);
if (!HasComp<MapComponent>(targetXform.GridUid))
+using Content.Shared.Parallax.Biomes.Layers;
+using Content.Shared.Parallax.Biomes.Markers;
using Robust.Shared.GameStates;
using Robust.Shared.Noise;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Parallax.Biomes;
[AutoNetworkedField]
public int Seed;
- [ViewVariables(VVAccess.ReadWrite),
- DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<BiomePrototype>))]
+ /// <summary>
+ /// The underlying entity, decal, and tile layers for the biome.
+ /// </summary>
+ [DataField("layers")]
[AutoNetworkedField]
- public string BiomePrototype = "Grasslands";
+ public List<IBiomeLayer> Layers = new();
- // TODO: Need to flag tiles as not requiring custom data anymore, e.g. if we spawn an ent and don't unspawn it.
+ /// <summary>
+ /// Templates to use for <see cref="Layers"/>. Optional as this can be set elsewhere.
+ /// </summary>
+ /// <remarks>
+ /// This is really just here for prototype reload support.
+ /// </remarks>
+ [ViewVariables(VVAccess.ReadWrite),
+ DataField("template", customTypeSerializer: typeof(PrototypeIdSerializer<BiomeTemplatePrototype>))]
+ public string? Template;
/// <summary>
/// If we've already generated a tile and couldn't deload it then we won't ever reload it in future.
[DataField("loadedChunks")]
public readonly HashSet<Vector2i> LoadedChunks = new();
+ #region Markers
+
/// <summary>
- /// Are we currently in the process of generating?
- /// Used to flag modified tiles without callers having to deal with it.
+ /// Track what markers we've loaded already to avoid double-loading.
/// </summary>
- public bool Generating = false;
+ [DataField("loadedMarkers", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<Vector2i>, BiomeMarkerLayerPrototype>))]
+ public readonly Dictionary<string, HashSet<Vector2i>> LoadedMarkers = new();
+
+ [DataField("markerLayers", customTypeSerializer: typeof(PrototypeIdListSerializer<BiomeMarkerLayerPrototype>))]
+ public List<string> MarkerLayers = new();
+
+ #endregion
}
+++ /dev/null
-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();
-}
--- /dev/null
+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();
+}
--- /dev/null
+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();
+}
--- /dev/null
+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; }
+}
--- /dev/null
+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();
+}
--- /dev/null
+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;
+}
--- /dev/null
+using Robust.Shared.Noise;
+
+namespace Content.Shared.Parallax.Biomes.Layers;
+
+[ImplicitDataDefinitionForInheritors]
+public interface IBiomeLayer
+{
+ /// <summary>
+ /// Seed is used an offset from the relevant BiomeComponent's seed.
+ /// </summary>
+ FastNoiseLite Noise { get; }
+
+ /// <summary>
+ /// Threshold for this layer to be present. If set to 0 forces it for every tile.
+ /// </summary>
+ float Threshold { get; }
+
+ /// <summary>
+ /// Is the thresold inverted so we need to be lower than it.
+ /// </summary>
+ public bool Invert { get; }
+}
\ No newline at end of file
--- /dev/null
+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; }
+}
--- /dev/null
+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;
+}
--- /dev/null
+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; }
+}
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Maps;
-using Robust.Shared.GameStates;
+using Content.Shared.Parallax.Biomes.Layers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Parallax.Biomes;
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)
{
return false;
}
- return TryGetBiomeTile(indices, ProtoManager.Index<BiomePrototype>(biome.BiomePrototype),
- biome.Noise, grid, out tile);
+ return TryGetBiomeTile(indices, biome.Layers, biome.Noise, grid, out tile);
}
/// <summary>
/// Tries to get the tile, real or otherwise, for the specified indices.
/// </summary>
- public bool TryGetBiomeTile(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
+ public bool TryGetBiomeTile(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent? grid, [NotNullWhen(true)] out Tile? tile)
{
if (grid?.TryGetTileRef(indices, out var tileRef) == true && !tileRef.Tile.IsEmpty)
{
var oldSeed = noise.GetSeed();
- for (var i = prototype.Layers.Count - 1; i >= 0; i--)
+ for (var i = layers.Count - 1; i >= 0; i--)
{
- var layer = prototype.Layers[i];
+ var layer = layers[i];
if (layer is not BiomeTileLayer tileLayer)
continue;
SetNoise(noise, oldSeed, layer.Noise);
- if (TryGetTile(indices, noise, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
+ if (TryGetTile(indices, noise, tileLayer.Invert, tileLayer.Threshold, ProtoManager.Index<ContentTileDefinition>(tileLayer.Tile), tileLayer.Variants, out tile))
{
noise.SetSeed(oldSeed);
return true;
/// <summary>
/// Gets the underlying biome tile, ignoring any existing tile that may be there.
/// </summary>
- private bool TryGetTile(Vector2i indices, FastNoiseLite seed, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
+ private bool TryGetTile(Vector2i indices, FastNoiseLite seed, bool invert, float threshold, ContentTileDefinition tileDef, List<byte>? variants, [NotNullWhen(true)] out Tile? tile)
{
var found = seed.GetNoise(indices.X, indices.Y);
+ found = invert ? found * -1 : found;
if (found < threshold)
{
/// <summary>
/// Tries to get the relevant entity for this tile.
/// </summary>
- protected bool TryGetEntity(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid,
+ protected bool TryGetEntity(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent grid,
[NotNullWhen(true)] out string? entity)
{
- if (!TryGetBiomeTile(indices, prototype, noise, grid, out var tileRef))
+ if (!TryGetBiomeTile(indices, layers, noise, grid, out var tileRef))
{
entity = null;
return false;
var tileId = TileDefManager[tileRef.Value.TypeId].ID;
var oldSeed = noise.GetSeed();
- for (var i = prototype.Layers.Count - 1; i >= 0; i--)
+ for (var i = layers.Count - 1; i >= 0; i--)
{
- var layer = prototype.Layers[i];
+ var layer = layers[i];
// Decals might block entity so need to check if there's one in front of us.
switch (layer)
{
+ case BiomeDummyLayer:
+ continue;
case IBiomeWorldLayer worldLayer:
if (!worldLayer.AllowedTiles.Contains(tileId))
continue;
}
SetNoise(noise, oldSeed, layer.Noise);
+ var invert = layer.Invert;
var value = noise.GetNoise(indices.X, indices.Y);
+ value = invert ? value * -1 : value;
if (value < layer.Threshold)
{
/// <summary>
/// Tries to get the relevant decals for this tile.
/// </summary>
- public bool TryGetDecals(Vector2i indices, BiomePrototype prototype, FastNoiseLite noise, MapGridComponent grid,
+ public bool TryGetDecals(Vector2i indices, List<IBiomeLayer> layers, FastNoiseLite noise, MapGridComponent grid,
[NotNullWhen(true)] out List<(string ID, Vector2 Position)>? decals)
{
- if (!TryGetBiomeTile(indices, prototype, noise, grid, out var tileRef))
+ if (!TryGetBiomeTile(indices, layers, noise, grid, out var tileRef))
{
decals = null;
return false;
var tileId = TileDefManager[tileRef.Value.TypeId].ID;
var oldSeed = noise.GetSeed();
- for (var i = prototype.Layers.Count - 1; i >= 0; i--)
+ for (var i = layers.Count - 1; i >= 0; i--)
{
- var layer = prototype.Layers[i];
+ var layer = layers[i];
// Entities might block decal so need to check if there's one in front of us.
switch (layer)
{
+ case BiomeDummyLayer:
+ continue;
case IBiomeWorldLayer worldLayer:
if (!worldLayer.AllowedTiles.Contains(tileId))
continue;
}
SetNoise(noise, oldSeed, layer.Noise);
+ var invert = layer.Invert;
// Check if the other layer should even render, if not then keep going.
if (layer is not BiomeDecalLayer decalLayer)
{
- if (noise.GetNoise(indices.X, indices.Y) < layer.Threshold)
+ var value = noise.GetNoise(indices.X, indices.Y);
+ value = invert ? value * -1 : value;
+
+ if (value < layer.Threshold)
continue;
decals = null;
{
var index = new Vector2(indices.X + x * 1f / decalLayer.Divisions, indices.Y + y * 1f / decalLayer.Divisions);
var decalValue = noise.GetNoise(index.X, index.Y);
+ decalValue = invert ? decalValue * -1 : decalValue;
if (decalValue < decalLayer.Threshold)
continue;
public sealed class Dungeon
{
+ /// <summary>
+ /// Starting position used to generate the dungeon from.
+ /// </summary>
+ public Vector2i Position;
+
+ public Vector2i Center;
+
public List<DungeonRoom> Rooms = new();
/// <summary>
--- /dev/null
+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;
+}
+++ /dev/null
-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;
-}
--- /dev/null
+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;
+}
[ImplicitDataDefinitionForInheritors]
public interface IDungeonLoot
{
- string Prototype { get; }
}
+using Content.Shared.Salvage;
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Procedural.Loot;
[DataField("desc")] public string Description = string.Empty;
+ /// <summary>
+ /// Mission types this loot is not allowed to spawn for
+ /// </summary>
+ [DataField("blacklist")]
+ public List<SalvageMissionType> Blacklist = new();
+
/// <summary>
/// All of the loot rules
/// </summary>
+++ /dev/null
-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;
-}
+++ /dev/null
-namespace Content.Shared.Procedural.Rewards;
-
-[ImplicitDataDefinitionForInheritors]
-public interface ISalvageReward
-{
-
-}
+++ /dev/null
-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!;
-}
+++ /dev/null
-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;
-}
+++ /dev/null
-namespace Content.Shared.Salvage.Expeditions;
-
-
-public interface IFactionExpeditionConfig
-{
-
-}
--- /dev/null
+namespace Content.Shared.Salvage.Expeditions.Modifiers;
+
+public interface ISalvageMod
+{
+ /// <summary>
+ /// Player-friendly version describing this modifier.
+ /// </summary>
+ string Description { get; }
+
+ float Cost { get; }
+}
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+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();
+}
+using Content.Shared.Salvage.Expeditions.Modifiers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Salvage.Expeditions;
[Prototype("salvageFaction")]
-public sealed class SalvageFactionPrototype : IPrototype
+public sealed class SalvageFactionPrototype : IPrototype, ISalvageMod
{
[IdDataField] public string ID { get; } = default!;
+ [DataField("desc")] public string Description { get; } = string.Empty;
+
+ /// <summary>
+ /// Cost for difficulty modifiers.
+ /// </summary>
+ [DataField("cost")]
+ public float Cost { get; } = 0f;
+
[ViewVariables(VVAccess.ReadWrite), DataField("groups", required: true)]
public List<SalvageMobGroup> MobGroups = default!;
/// <summary>
- /// Per expedition type data for this faction.
+ /// Miscellaneous data for factions.
/// </summary>
- [ViewVariables(VVAccess.ReadWrite), DataField("configs", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<IFactionExpeditionConfig, SalvageExpeditionPrototype>))]
- public Dictionary<string, IFactionExpeditionConfig> Configs = new();
+ [ViewVariables(VVAccess.ReadWrite), DataField("configs")]
+ public Dictionary<string, string> Configs = new();
}
+++ /dev/null
-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;
-}
+++ /dev/null
-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;
-}
+++ /dev/null
-namespace Content.Shared.Salvage;
-
-public interface ISalvageMission {}
\ No newline at end of file
+++ /dev/null
-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,
-}
-
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Salvage;
{
public TimeSpan NextOffer;
public bool Claimed;
+ public bool Cooldown;
public ushort ActiveMission;
- public List<SalvageMission> Missions;
+ public List<SalvageMissionParams> Missions;
- public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, ushort activeMission, List<SalvageMission> missions)
+ public SalvageExpeditionConsoleState(TimeSpan nextOffer, bool claimed, bool cooldown, ushort activeMission, List<SalvageMissionParams> missions)
{
NextOffer = nextOffer;
Claimed = claimed;
+ Cooldown = cooldown;
ActiveMission = activeMission;
Missions = missions;
}
[ViewVariables]
public bool Claimed => ActiveMission != 0;
+ /// <summary>
+ /// Are we actively cooling down from the last salvage mission.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
+ public bool Cooldown = false;
+
/// <summary>
/// Nexy time salvage missions are offered.
/// </summary>
public TimeSpan NextOffer;
[ViewVariables]
- public readonly Dictionary<ushort, SalvageMission> Missions = new();
+ public readonly Dictionary<ushort, SalvageMissionParams> Missions = new();
[ViewVariables] public ushort ActiveMission;
}
[Serializable, NetSerializable]
-public sealed record SalvageMission
+public sealed record SalvageMissionParams
{
[ViewVariables]
public ushort Index;
- [ViewVariables(VVAccess.ReadWrite), DataField("config", required: true, customTypeSerializer:typeof(SalvageExpeditionPrototype))]
- public string Config = default!;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public SalvageMissionType MissionType;
- [ViewVariables] public TimeSpan Duration;
+ [ViewVariables(VVAccess.ReadWrite)] public int Seed;
- [ViewVariables] public int Seed;
+ /// <summary>
+ /// Base difficulty for this mission.
+ /// </summary>
+ [ViewVariables(VVAccess.ReadWrite)] public DifficultyRating Difficulty;
}
-[Serializable, NetSerializable]
-public enum SalvageEnvironment : byte
+/// <summary>
+/// Created from <see cref="SalvageMissionParams"/>. Only needed for data the client also needs for mission
+/// display.
+/// </summary>
+public sealed record SalvageMission(
+ int Seed,
+ DifficultyRating Difficulty,
+ string Dungeon,
+ string Faction,
+ SalvageMissionType Mission,
+ string Biome,
+ Color? Color,
+ TimeSpan Duration,
+ Dictionary<string, int> Loot,
+ List<string> Modifiers)
{
- Invalid = 0,
- Caves,
+ /// <summary>
+ /// Seed used for the mission.
+ /// </summary>
+ public readonly int Seed = Seed;
+
+ /// <summary>
+ /// Difficulty rating.
+ /// </summary>
+ public DifficultyRating Difficulty = Difficulty;
+
+ /// <summary>
+ /// <see cref="SalvageDungeonMod"/> to be used.
+ /// </summary>
+ public readonly string Dungeon = Dungeon;
+
+ /// <summary>
+ /// <see cref="SalvageFactionPrototype"/> to be used.
+ /// </summary>
+ public readonly string Faction = Faction;
+
+ /// <summary>
+ /// Underlying mission params that generated this.
+ /// </summary>
+ public readonly SalvageMissionType Mission = Mission;
+
+ /// <summary>
+ /// Biome to be used for the mission.
+ /// </summary>
+ public readonly string Biome = Biome;
+
+ /// <summary>
+ /// Lighting color to be used (AKA outdoor lighting).
+ /// </summary>
+ public readonly Color? Color = Color;
+
+ /// <summary>
+ /// Mission duration.
+ /// </summary>
+ public TimeSpan Duration = Duration;
+
+ public Dictionary<string, int> Loot = Loot;
+
+ /// <summary>
+ /// Modifiers (outside of the above) applied to the mission.
+ /// </summary>
+ public List<string> Modifiers = Modifiers;
}
[Serializable, NetSerializable]
+using System.Linq;
using Content.Shared.Dataset;
using Content.Shared.Procedural.Loot;
-using Content.Shared.Procedural.Rewards;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
-using Content.Shared.Salvage.Expeditions.Structure;
+using Content.Shared.Salvage.Expeditions;
+using Content.Shared.Salvage.Expeditions.Modifiers;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
namespace Content.Shared.Salvage;
public abstract class SharedSalvageSystem : EntitySystem
{
+ [Dependency] private readonly ILocalizationManager _loc = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
public static readonly TimeSpan MissionCooldown = TimeSpan.FromMinutes(5);
- public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(10);
+ public static readonly TimeSpan MissionFailedCooldown = TimeSpan.FromMinutes(15);
+
+ #region Descriptions
- public static float GetDifficultyModifier(DifficultyRating difficulty)
+ public string GetMissionDescription(SalvageMission mission)
{
- // These should reflect how many salvage staff are expected to be required for the mission.
- switch (difficulty)
+ // Hardcoded in coooooz it's dynamic based on difficulty and I'm lazy.
+ switch (mission.Mission)
+ {
+ case SalvageMissionType.Mining:
+ // Taxation: , ("tax", $"{GetMiningTax(mission.Difficulty) * 100f:0}")
+ return Loc.GetString("salvage-expedition-desc-mining");
+ case SalvageMissionType.Destruction:
+ var proto = _proto.Index<SalvageFactionPrototype>(mission.Faction).Configs["DefenseStructure"];
+
+ return Loc.GetString("salvage-expedition-desc-structure",
+ ("count", GetStructureCount(mission.Difficulty)),
+ ("structure", _loc.GetEntityData(proto).Name));
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ public float GetMiningTax(DifficultyRating baseRating)
+ {
+ return 0.6f + (int) baseRating * 0.05f;
+ }
+
+ /// <summary>
+ /// Gets the amount of structures to destroy.
+ /// </summary>
+ public int GetStructureCount(DifficultyRating baseRating)
+ {
+ return 1 + (int) baseRating * 2;
+ }
+
+ #endregion
+
+ public int GetDifficulty(DifficultyRating rating)
+ {
+ switch (rating)
{
case DifficultyRating.None:
- return 1f;
+ return 1;
case DifficultyRating.Minor:
- return 1.5f;
+ return 2;
case DifficultyRating.Moderate:
- return 3f;
+ return 4;
case DifficultyRating.Hazardous:
- return 6f;
+ return 6;
case DifficultyRating.Extreme:
- return 10f;
+ return 8;
default:
- throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null);
+ throw new ArgumentOutOfRangeException(nameof(rating), rating, null);
}
}
+ /// <summary>
+ /// How many groups of mobs to spawn for a mission.
+ /// </summary>
+ public float GetSpawnCount(DifficultyRating difficulty)
+ {
+ return (int) difficulty * 2;
+ }
+
public static string GetFTLName(DatasetPrototype dataset, int seed)
{
var random = new System.Random(seed);
return $"{dataset.Values[random.Next(dataset.Values.Count)]}-{random.Next(10, 100)}-{(char) (65 + random.Next(26))}";
}
- public static string GetFaction(List<string> factions, int seed)
+ public SalvageMission GetMission(SalvageMissionType config, DifficultyRating difficulty, int seed)
{
- var adjustedSeed = new System.Random(seed + 1);
- return factions[adjustedSeed.Next(factions.Count)];
+ // This is on shared to ensure the client display for missions and what the server generates are consistent
+ var rating = (float) GetDifficulty(difficulty);
+ // Don't want easy missions to have any negative modifiers but also want
+ // easy to be a 1 for difficulty.
+ rating -= 1f;
+ var rand = new System.Random(seed);
+ var faction = GetMod<SalvageFactionPrototype>(rand, ref rating);
+ var biome = GetMod<SalvageBiomeMod>(rand, ref rating);
+ var dungeon = GetDungeon(biome.ID, rand, ref rating);
+ var mods = new List<string>();
+
+ SalvageLightMod? light = null;
+
+ if (biome.BiomePrototype != null)
+ {
+ light = GetLight(biome.ID, rand, ref rating);
+ mods.Add(light.Description);
+ }
+
+ var time = GetMod<SalvageTimeMod>(rand, ref rating);
+ // Round the duration to nearest 15 seconds.
+ var exactDuration = time.MinDuration + (time.MaxDuration - time.MinDuration) * rand.NextFloat();
+ exactDuration = MathF.Round(exactDuration / 15f) * 15f;
+ var duration = TimeSpan.FromSeconds(exactDuration);
+
+ if (time.ID != "StandardTime")
+ {
+ mods.Add(time.Description);
+ }
+
+ var loots = GetLoot(config, _proto.EnumeratePrototypes<SalvageLootPrototype>().ToList(), GetDifficulty(difficulty), seed);
+ return new SalvageMission(seed, difficulty, dungeon.ID, faction.ID, config, biome.ID, light?.Color, duration, loots, mods);
}
- public static IEnumerable<SalvageLootPrototype> GetLoot(List<string> loots, int seed, IPrototypeManager protoManager)
+ public SalvageDungeonMod GetDungeon(string biome, System.Random rand, ref float rating)
{
- var adjustedSeed = new System.Random(seed + 2);
+ var mods = _proto.EnumeratePrototypes<SalvageDungeonMod>().ToList();
+ mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
+ rand.Shuffle(mods);
- for (var i = 0; i < loots.Count; i++)
+ foreach (var mod in mods)
{
- var loot = loots[i];
- var a = protoManager.Index<WeightedRandomPrototype>(loot);
- var lootConfig = a.Pick(adjustedSeed);
- yield return protoManager.Index<SalvageLootPrototype>(lootConfig);
+ if (mod.BiomeMods?.Contains(biome) == false ||
+ mod.Cost > rating)
+ {
+ continue;
+ }
+
+ rating -= (int) mod.Cost;
+
+ return mod;
}
+
+ throw new InvalidOperationException();
}
- public static ISalvageReward GetReward(WeightedRandomPrototype proto, int seed, IPrototypeManager protoManager)
+ public SalvageLightMod GetLight(string biome, System.Random rand, ref float rating)
{
- var adjustedSeed = new System.Random(seed + 3);
- var rewardProto = proto.Pick(adjustedSeed);
- return protoManager.Index<SalvageRewardPrototype>(rewardProto).Reward;
+ var mods = _proto.EnumeratePrototypes<SalvageLightMod>().ToList();
+ mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
+ rand.Shuffle(mods);
+
+ foreach (var mod in mods)
+ {
+ if (mod.Biomes?.Contains(biome) == false || mod.Cost > rating)
+ continue;
+
+ rating -= mod.Cost;
+
+ return mod;
+ }
+
+ throw new InvalidOperationException();
}
- #region Structure
+ public T GetMod<T>(System.Random rand, ref float rating) where T : class, IPrototype, ISalvageMod
+ {
+ var mods = _proto.EnumeratePrototypes<T>().ToList();
+ mods.Sort((x, y) => string.Compare(x.ID, y.ID, StringComparison.Ordinal));
+ rand.Shuffle(mods);
+
+ foreach (var mod in mods)
+ {
+ if (mod.Cost > rating)
+ continue;
+
+ rating -= mod.Cost;
+
+ return mod;
+ }
+
+ throw new InvalidOperationException();
+ }
- public static int GetStructureCount(SalvageStructure structure, int seed)
+ private Dictionary<string, int> GetLoot(SalvageMissionType mission, List<SalvageLootPrototype> loots, int count, int seed)
{
- var adjustedSeed = new System.Random(seed + 4);
- return adjustedSeed.Next(structure.MinStructures, structure.MaxStructures + 1);
+ var results = new Dictionary<string, int>();
+ var adjustedSeed = new System.Random(seed + 2);
+
+ for (var i = 0; i < count; i++)
+ {
+ adjustedSeed.Shuffle(loots);
+
+ foreach (var loot in loots)
+ {
+ if (loot.Blacklist.Contains(mission))
+ continue;
+
+ var weh = results.GetOrNew(loot.ID);
+ weh++;
+ results[loot.ID] = weh;
+ break;
+ }
+ }
+
+ return results;
}
+}
- #endregion
+[Serializable, NetSerializable]
+public enum SalvageMissionType : byte
+{
+ /// <summary>
+ /// No dungeon, just ore loot and random mob spawns.
+ /// </summary>
+ Mining,
+
+ /// <summary>
+ /// Destroy the specified structures in a dungeon.
+ /// </summary>
+ Destruction,
+}
+
+[Serializable, NetSerializable]
+public enum DifficultyRating : byte
+{
+ None,
+ Minor,
+ Moderate,
+ Hazardous,
+ Extreme,
}
--- /dev/null
+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>
--- /dev/null
+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}.
- pos: 0.5,0.5\r
parent: 0\r
type: Transform\r
-- uid: 85\r
- type: PaperWrittenSalvageLoreMedium1PlasmaTrap\r
- components:\r
- - pos: 0.48327154,0.5698495\r
- parent: 0\r
- type: Transform\r
- uid: 86\r
type: ClothingEyesGlassesMeson\r
components:\r
- pos: -3.5,0.5\r
parent: 0\r
type: Transform\r
-- uid: 55\r
- type: SalvageLorePaperGamingSpawner\r
- components:\r
- - pos: -1.5,-2.5\r
- parent: 0\r
- type: Transform\r
- uid: 56\r
type: SalvageMobSpawner75\r
components:\r
+++ /dev/null
-# ---- 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
-# ----
- type: ComputerBoard
prototype: ComputerCargoShuttle
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: SalvageExpeditionsComputerCircuitboard
+ name: salvage expeditions computer board
+ description: A computer printed circuit board for a salvage expeditions computer.
+ components:
+ - type: Sprite
+ state: cpu_supply
+ - type: ComputerBoard
+ prototype: ComputerSalvageExpedition
+
- type: entity
parent: BaseComputerCircuitboard
id: CargoShuttleConsoleCircuitboard
damageContainer: Inorganic
damageModifierSet: StrongMetallic
+- type: entity
+ id: ComputerSalvageExpedition
+ parent: BaseComputer
+ name: salvage expeditions computer
+ description: Used take salvage missions.
+ components:
+ - type: Sprite
+ layers:
+ - map: ["computerLayerBody"]
+ state: computer
+ - map: ["computerLayerKeyboard"]
+ state: generic_keyboard
+ - map: [ "computerLayerScreen" ]
+ state: mining
+ - map: ["computerLayerKeys"]
+ state: tech_key
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.ComputerVisuals.Powered:
+ computerLayerScreen:
+ True: { visible: true, shader: unshaded }
+ False: { visible: false }
+ computerLayerKeys:
+ True: { visible: true, shader: unshaded }
+ False: { visible: true }
+ - type: SalvageExpeditionConsole
+ - type: ActivatableUI
+ key: enum.SalvageConsoleUiKey.Expedition
+ - type: ActivatableUIRequiresPower
+ - type: UserInterface
+ interfaces:
+ - key: enum.SalvageConsoleUiKey.Expedition
+ type: SalvageExpeditionConsoleBoundUserInterface
+ - type: Computer
+ board: SalvageExpeditionsComputerCircuitboard
+ - type: PointLight
+ radius: 1.5
+ energy: 1.6
+ color: "#b89f25"
+ - type: AccessReader
+ access: [["Salvage"]]
+
- type: entity
parent: BaseComputer
id: ComputerSurveillanceCameraMonitor
- type: entity
id: XenoWardingTower
name: Xeno warding tower
- description: a
placement:
mode: SnapgridCenter
snap:
Heat:
collection:
MeatLaserImpact
+ - type: Clickable
- type: InteractionOutline
- type: Sprite
netsync: false
--- /dev/null
+- 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
--- /dev/null
+# 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
# Desert
# TODO: Water in desert
-- type: biome
+- type: biomeTemplate
id: LowDesert
- desc: Desert
layers:
- !type:BiomeEntityLayer
threshold: 0.95
- FloorLowDesert
entities:
- AsteroidRock
+ - !type:BiomeDummyLayer
+ id: Loot
# Fill layer
- !type:BiomeTileLayer
threshold: -1
frequency: 0.1
# Grass
-- type: biome
+- type: biomeTemplate
id: Grasslands
- desc: Grasslands
layers:
# Sparse vegetation
- !type:BiomeDecalLayer
cellularReturnType: Distance2
entities:
- WallRock
+ - !type:BiomeDummyLayer
+ id: Loot
# Water
- !type:BiomeEntityLayer
allowedTiles:
cellularReturnType: Distance2
# Lava
-- type: biome
+- type: biomeTemplate
id: Lava
- desc: Lava
layers:
- !type:BiomeEntityLayer
threshold: 0.9
- FloorBasalt
entities:
- FloorLavaEntity
+ - !type:BiomeDummyLayer
+ id: Loot
# Fill basalt
- !type:BiomeTileLayer
threshold: -1
tile: FloorBasalt
# Snow
-- type: biome
+- type: biomeTemplate
id: Snow # Similar to Grasslands... but snow
layers:
# Sparse vegetation
- FloraTreeSnow04
- FloraTreeSnow05
- FloraTreeSnow06
+ - !type:BiomeDummyLayer
+ id: Loot
- !type:BiomeTileLayer
threshold: -1.0
tile: FloorSnow
seed: 0
frequency: 0.02
fractalType: None
+
+# Caves
+- type: biomeTemplate
+ id: Caves
+ layers:
+ - !type:BiomeEntityLayer
+ threshold: -0.5
+ invert: true
+ noise:
+ seed: 0
+ noiseType: Perlin
+ fractalType: Ridged
+ octaves: 1
+ frequency: 0.1
+ gain: 0
+ allowedTiles:
+ - FloorAsteroidSand
+ entities:
+ - WallRock
+ - !type:BiomeDummyLayer
+ id: Loot
+ - !type:BiomeTileLayer
+ threshold: -1.0
+ tile: FloorAsteroidSand
id: SmallArea1
size: 5,5
rooms:
- - 0,0,5,5
+ - 0,0,5,5
\ No newline at end of file
--- /dev/null
+- 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
--- /dev/null
+# 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
--- /dev/null
+# 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