Loadouts redux (#25715)

* Loadouts redux

* Loadout window mockup

* More workout

* rent

* validation

* Developments

* bcs

* More cleanup

* Rebuild working

* Fix model and loading

* obsession

* efcore

* We got a stew goin

* Cleanup

* Optional + SeniorEngineering fix

* Fixes

* Update science.yml

* add

add

* Automatic naming

* Update nukeops

* Coming together

* Right now

* stargate

* rejig the UI

* weh

* Loadouts tweaks

* Merge conflicts + ordering fix

* yerba mate

* chocolat

* More updates

* Add multi-selection support

* test

h

* fikss

* a

* add tech assistant and hazard suit

* huh

* Latest changes

* add medical loadouts

* and science

* finish security loadouts

* cargo

* service done

* added wildcards

* add command

* Move restrictions

* Finalising

* Fix existing work

* Localise next batch

* clothing fix

* Fix storage names

* review

* the scooping room

* Test fixes

* Xamlify

* Xamlify this too

* Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* ben

* Margins

---------

Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com>
Co-authored-by: Mr. 27 <koolthunder019@gmail.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
This commit is contained in:
metalgearsloth
2024-04-16 22:57:43 +10:00
committed by GitHub
parent fff3fe2a24
commit 12766fe6e3
155 changed files with 14277 additions and 1270 deletions

View File

@@ -58,7 +58,7 @@ public class SpawnEquipDeleteBenchmark
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
_spawnSys.EquipStartingGear(_entity, _gear, null);
_spawnSys.EquipStartingGear(_entity, _gear);
server.EntMan.DeleteEntity(_entity);
}
});

View File

@@ -21,6 +21,7 @@ using Content.Shared.Module;
using Content.Client.Guidebook;
using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking;
namespace Content.Client.IoC
@@ -29,26 +30,29 @@ namespace Content.Client.IoC
{
public static void Register()
{
IoCManager.Register<IParallaxManager, ParallaxManager>();
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
IoCManager.Register<IStylesheetManager, StylesheetManager>();
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
IoCManager.Register<FullscreenHook, FullscreenHook>();
IoCManager.Register<IClickMapManager, ClickMapManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<ChangelogManager, ChangelogManager>();
IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<ViewportManager, ViewportManager>();
IoCManager.Register<ISharedAdminLogManager, SharedAdminLogManager>();
IoCManager.Register<GhostKickManager>();
IoCManager.Register<ExtendedDisconnectInformationManager>();
IoCManager.Register<JobRequirementsManager>();
IoCManager.Register<DocumentParsingManager>();
IoCManager.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
var collection = IoCManager.Instance!;
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
collection.Register<IStylesheetManager, StylesheetManager>();
collection.Register<IScreenshotHook, ScreenshotHook>();
collection.Register<FullscreenHook, FullscreenHook>();
collection.Register<IClickMapManager, ClickMapManager>();
collection.Register<IClientAdminManager, ClientAdminManager>();
collection.Register<ISharedAdminManager, ClientAdminManager>();
collection.Register<EuiManager, EuiManager>();
collection.Register<IVoteManager, VoteManager>();
collection.Register<ChangelogManager, ChangelogManager>();
collection.Register<RulesManager, RulesManager>();
collection.Register<ViewportManager, ViewportManager>();
collection.Register<ISharedAdminLogManager, SharedAdminLogManager>();
collection.Register<GhostKickManager>();
collection.Register<ExtendedDisconnectInformationManager>();
collection.Register<JobRequirementsManager>();
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
}
}
}

View File

@@ -70,7 +70,7 @@ namespace Content.Client.Lobby
_characterSetup.SaveButton.OnPressed += _ =>
{
_characterSetup.Save();
_lobby.CharacterPreview.UpdateUI();
_userInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
};
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
@@ -84,10 +84,6 @@ namespace Content.Client.Lobby
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
_gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
_lobby.CharacterPreview.UpdateUI();
}
protected override void Shutdown()
@@ -109,13 +105,6 @@ namespace Content.Client.Lobby
_characterSetup?.Dispose();
_characterSetup = null;
_preferencesManager.OnServerDataLoaded -= PreferencesDataLoaded;
}
private void PreferencesDataLoaded()
{
_lobby?.CharacterPreview.UpdateUI();
}
private void OnSetupPressed(BaseButton.ButtonEventArgs args)

View File

@@ -0,0 +1,223 @@
using System.Linq;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Lobby.UI;
using Content.Client.Preferences;
using Content.Client.Station;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby;
public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
{
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
private LobbyCharacterPreviewPanel? _previewPanel;
/*
* Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
* that is shared too.
*/
/// <summary>
/// Preview dummy for role gear.
/// </summary>
private EntityUid? _previewDummy;
/// <summary>
/// If we currently have a loadout selected.
/// </summary>
private JobPrototype? _dummyJob;
// TODO: Load the species directly and don't update entity ever.
public event Action<EntityUid>? PreviewDummyUpdated;
public override void Initialize()
{
base.Initialize();
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
}
private void PreferencesDataLoaded()
{
UpdateCharacterUI();
}
public void OnStateEntered(LobbyState state)
{
}
public void OnStateExited(LobbyState state)
{
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
}
public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
{
_previewPanel = panel;
UpdateCharacterUI();
}
public void SetDummyJob(JobPrototype? job)
{
_dummyJob = job;
UpdateCharacterUI();
}
public void UpdateCharacterUI()
{
// Test moment
if (_stateManager.CurrentState is not LobbyState)
return;
if (!_preferencesManager.ServerDataLoaded)
{
_previewPanel?.SetLoaded(false);
return;
}
_previewPanel?.SetLoaded(true);
if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
{
_previewPanel?.SetSummaryText(string.Empty);
}
else
{
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
_previewPanel?.SetSprite(_previewDummy.Value);
_previewPanel?.SetSummaryText(selectedCharacter.Summary);
_humanoid.LoadProfile(_previewDummy.Value, selectedCharacter);
GiveDummyJobClothesLoadout(_previewDummy.Value, selectedCharacter);
PreviewDummyUpdated?.Invoke(_previewDummy.Value);
}
}
/// <summary>
/// Applies the highest priority job's clothes to the dummy.
/// </summary>
public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
{
var job = _dummyJob ?? GetPreferredJob(profile);
GiveDummyJobClothes(dummy, profile, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager);
GiveDummyLoadout(dummy, loadout);
}
}
/// <summary>
/// Gets the highest priority job for the profile.
/// </summary>
public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
{
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
return _prototypeManager.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
}
public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
{
if (roleLoadout == null)
return;
foreach (var group in roleLoadout.SelectedLoadouts.Values)
{
foreach (var loadout in group)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
_spawn.EquipStartingGear(uid, _prototypeManager.Index(loadoutProto.Equipment));
}
}
}
/// <summary>
/// Applies the specified job's clothes to the dummy.
/// </summary>
public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
{
if (!_inventory.TryGetSlots(dummy, out var slots))
return;
// Apply loadout
if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
{
foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
{
foreach (var loadout in loadouts)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
// TODO: Need some way to apply starting gear to an entity coz holy fucking shit dude.
var loadoutGear = _prototypeManager.Index(loadoutProto.Equipment);
foreach (var slot in slots)
{
var itemType = loadoutGear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
if (job.StartingGear == null)
return;
var gear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
public EntityUid? GetPreviewDummy()
{
return _previewDummy;
}
}

View File

@@ -1,166 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.Alerts;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Preferences;
using Content.Client.UserInterface.Controls;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Lobby.UI
{
public sealed class LobbyCharacterPreviewPanel : Control
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private EntityUid? _previewDummy;
private readonly Label _summaryLabel;
private readonly BoxContainer _loaded;
private readonly BoxContainer _viewBox;
private readonly Label _unloaded;
public LobbyCharacterPreviewPanel()
{
IoCManager.InjectDependencies(this);
var header = new NanoHeading
{
Text = Loc.GetString("lobby-character-preview-panel-header")
};
CharacterSetupButton = new Button
{
Text = Loc.GetString("lobby-character-preview-panel-character-setup-button"),
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(0, 5, 0, 0),
};
_summaryLabel = new Label
{
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(3, 3),
};
var vBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
};
_unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
_loaded = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Visible = false
};
_viewBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Center,
};
var _vSpacer = new VSpacer();
_loaded.AddChild(_summaryLabel);
_loaded.AddChild(_viewBox);
_loaded.AddChild(_vSpacer);
_loaded.AddChild(CharacterSetupButton);
vBox.AddChild(header);
vBox.AddChild(_loaded);
vBox.AddChild(_unloaded);
AddChild(vBox);
UpdateUI();
}
public Button CharacterSetupButton { get; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
if (_previewDummy != null) _entityManager.DeleteEntity(_previewDummy.Value);
_previewDummy = default;
}
public void UpdateUI()
{
if (!_preferencesManager.ServerDataLoaded)
{
_loaded.Visible = false;
_unloaded.Visible = true;
}
else
{
_loaded.Visible = true;
_unloaded.Visible = false;
if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
{
_summaryLabel.Text = string.Empty;
}
else
{
_previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
_viewBox.DisposeAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(4f, 4f),
MaxSize = new Vector2(112, 112),
Stretch = SpriteView.StretchMode.Fill,
};
spriteView.SetEntity(_previewDummy.Value);
_viewBox.AddChild(spriteView);
_summaryLabel.Text = selectedCharacter.Summary;
_entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
}
}
}
public static void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile)
{
var protoMan = IoCManager.Resolve<IPrototypeManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var invSystem = EntitySystem.Get<ClientInventorySystem>();
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
var job = protoMan.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
if (job.StartingGear != null && invSystem.TryGetSlots(dummy, out var slots))
{
var gear = protoMan.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name, profile);
if (invSystem.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
entMan.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = entMan.SpawnEntity(itemType, MapCoordinates.Nullspace);
invSystem.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
<Control
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Name="VBox" Orientation="Vertical">
<controls:NanoHeading Name="Header" Text="{Loc 'lobby-character-preview-panel-header'}">
</controls:NanoHeading>
<BoxContainer Name="Loaded" Orientation="Vertical"
Visible="False">
<Label Name="Summary" HorizontalAlignment="Center" Margin="3 3"/>
<BoxContainer Name="ViewBox" Orientation="Horizontal" HorizontalAlignment="Center">
</BoxContainer>
<controls:VSpacer/>
<Button Name="CharacterSetup" Text="{Loc 'lobby-character-preview-panel-character-setup-button'}"
HorizontalAlignment="Center"
Margin="0 5 0 0"/>
</BoxContainer>
<Label Name="Unloaded" Text="{Loc 'lobby-character-preview-panel-unloaded-preferences-label'}"/>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,45 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences]
public sealed partial class LobbyCharacterPreviewPanel : Control
{
public Button CharacterSetupButton => CharacterSetup;
public LobbyCharacterPreviewPanel()
{
RobustXamlLoader.Load(this);
UserInterfaceManager.GetUIController<LobbyUIController>().SetPreviewPanel(this);
}
public void SetLoaded(bool value)
{
Loaded.Visible = value;
Unloaded.Visible = !value;
}
public void SetSummaryText(string value)
{
Summary.Text = string.Empty;
}
public void SetSprite(EntityUid uid)
{
ViewBox.DisposeAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(4f, 4f),
MaxSize = new Vector2(112, 112),
Stretch = SpriteView.StretchMode.Fill,
};
spriteView.SetEntity(uid);
ViewBox.AddChild(spriteView);
}
}

View File

@@ -1,23 +1,9 @@
using Content.Client.Chat.UI;
using Content.Client.Info;
using Content.Client.Message;
using Content.Client.Preferences;
using Content.Client.Preferences.UI;
using Content.Client.UserInterface.Screens;
using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Lobby.UI
{

View File

@@ -7,12 +7,13 @@ using Robust.Client;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Players.PlayTimeTracking;
public sealed class JobRequirementsManager
public sealed class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
@@ -133,5 +134,13 @@ public sealed class JobRequirementsManager
}
}
public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
{
if (session != _playerManager.LocalSession)
{
return new Dictionary<string, TimeSpan>();
}
return _roles;
}
}

View File

@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Preferences;
using Robust.Client;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -20,8 +18,7 @@ namespace Content.Client.Preferences
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public event Action? OnServerDataLoaded;
@@ -64,7 +61,8 @@ namespace Content.Client.Preferences
public void UpdateCharacter(ICharacterProfile profile, int slot)
{
profile.EnsureValid(_cfg, _prototypes);
var collection = IoCManager.Instance!;
profile.EnsureValid(_playerManager.LocalSession!, collection);
var characters = new Dictionary<int, ICharacterProfile>(Preferences.Characters) {[slot] = profile};
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor);
var msg = new MsgUpdateCharacter

View File

@@ -0,0 +1,41 @@
using Content.Client.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Preferences.UI;
public sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
// Not supported yet get fucked.
Setup(null, items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}

View File

@@ -40,7 +40,7 @@
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Name="CharEditor" />
<BoxContainer Name="CharEditor" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -3,27 +3,23 @@ using System.Numerics;
using Content.Client.Humanoid;
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
using Content.Client.Lobby.UI;
using Content.Client.Lobby;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
@@ -36,7 +32,6 @@ namespace Content.Client.Preferences.UI
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager;
private readonly IConfigurationManager _configurationManager;
private readonly Button _createNewCharacterButton;
private readonly HumanoidProfileEditor _humanoidProfileEditor;
@@ -51,7 +46,6 @@ namespace Content.Client.Preferences.UI
_entityManager = entityManager;
_prototypeManager = prototypeManager;
_preferencesManager = preferencesManager;
_configurationManager = configurationManager;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
@@ -74,7 +68,7 @@ namespace Content.Client.Preferences.UI
args.Event.Handle();
};
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, entityManager, configurationManager);
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
_humanoidProfileEditor.OnProfileChanged += ProfileChanged;
CharEditor.AddChild(_humanoidProfileEditor);
@@ -105,6 +99,7 @@ namespace Content.Client.Preferences.UI
private void UpdateUI()
{
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
var numberOfFullSlots = 0;
var characterButtonsGroup = new ButtonGroup();
Characters.RemoveAllChildren();
@@ -120,11 +115,6 @@ namespace Content.Client.Preferences.UI
foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
{
if (character is null)
{
continue;
}
numberOfFullSlots++;
var characterPickerButton = new CharacterPickerButton(_entityManager,
_preferencesManager,
@@ -148,8 +138,12 @@ namespace Content.Client.Preferences.UI
_createNewCharacterButton.Disabled =
numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
Characters.AddChild(_createNewCharacterButton);
// TODO: Move this shit to the Lobby UI controller
}
/// <summary>
/// Shows individual characters on the side of the character GUI.
/// </summary>
private sealed class CharacterPickerButton : ContainerButton
{
private EntityUid _previewDummy;
@@ -180,7 +174,15 @@ namespace Content.Client.Preferences.UI
if (humanoid != null)
{
LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy, humanoid);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
var job = controller.GetPreferredJob(humanoid);
controller.GiveDummyJobClothes(_previewDummy, humanoid, job);
if (prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager);
controller.GiveDummyLoadout(_previewDummy, loadout);
}
}
var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;

View File

@@ -0,0 +1,11 @@
<PanelContainer
xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2F2F35"
ContentMarginTopOverride="10"
ContentMarginBottomOverride="10"
ContentMarginLeftOverride="10"
ContentMarginRightOverride="10"/>
</PanelContainer.PanelOverride>
</PanelContainer>

View File

@@ -0,0 +1,14 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class HighlightedContainer : PanelContainer
{
public HighlightedContainer()
{
RobustXamlLoader.Load(this);
}
}

View File

@@ -5,8 +5,6 @@ namespace Content.Client.Preferences.UI
{
public sealed partial class HumanoidProfileEditor
{
private readonly IPrototypeManager _prototypeManager;
private void RandomizeEverything()
{
Profile = HumanoidCharacterProfile.Random();

View File

@@ -1,11 +1,11 @@
<Control xmlns="https://spacestation14.io"
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prefUi="clr-namespace:Content.Client.Preferences.UI"
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<BoxContainer Orientation="Horizontal">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
HorizontalExpand="True">
<!-- Left side -->
<BoxContainer Orientation="Vertical" Margin="10 10 10 10">
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
<!-- Middle container -->
<BoxContainer Orientation="Horizontal" SeparationOverride="10">
<!-- Name box-->
@@ -58,7 +58,9 @@
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-species-label'}" />
<Control HorizontalExpand="True"/>
<TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3" VerticalAlignment="Center"></TextureButton>
<TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3"
VerticalAlignment="Center"
ToolTip="{Loc 'humanoid-profile-editor-guidebook-button-tooltip'}"/>
<OptionButton Name="CSpeciesButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Age -->
@@ -85,18 +87,6 @@
<Control HorizontalExpand="True"/>
<Button Name="ShowClothes" Pressed="True" ToggleMode="True" Text="{Loc 'humanoid-profile-editor-clothing-show'}" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Clothing -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-clothing-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="CClothingButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Backpack -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-backpack-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="CBackpackButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Spawn Priority -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-spawn-priority-label'}" />
@@ -151,7 +141,7 @@
</TabContainer>
</BoxContainer>
<!-- Right side -->
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Center">
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<SpriteView Name="CSpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
<Button Name="CSpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
@@ -159,5 +149,4 @@
<Button Name="CSpriteRotateRight" Text="▶" StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</Control>
</BoxContainer>

View File

@@ -2,69 +2,48 @@ using System.Linq;
using System.Numerics;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Lobby.UI;
using Content.Client.Lobby;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Preferences.UI
{
public sealed class HighlightedContainer : PanelContainer
{
public HighlightedContainer()
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = new Color(47, 47, 53),
ContentMarginTopOverride = 10,
ContentMarginBottomOverride = 10,
ContentMarginLeftOverride = 10,
ContentMarginRightOverride = 10
};
}
}
[GenerateTypedNameReferences]
public sealed partial class HumanoidProfileEditor : Control
public sealed partial class HumanoidProfileEditor : BoxContainer
{
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entMan;
private readonly IConfigurationManager _configurationManager;
private readonly IPrototypeManager _prototypeManager;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private LineEdit _ageEdit => CAgeEdit;
private LineEdit _nameEdit => CNameEdit;
private TextEdit _flavorTextEdit = null!;
private TextEdit? _flavorTextEdit;
private Button _nameRandomButton => CNameRandomize;
private Button _randomizeEverythingButton => CRandomizeEverything;
private RichTextLabel _warningLabel => CWarningLabel;
@@ -72,8 +51,6 @@ namespace Content.Client.Preferences.UI
private OptionButton _sexButton => CSexButton;
private OptionButton _genderButton => CPronounsButton;
private Slider _skinColor => CSkin;
private OptionButton _clothingButton => CClothingButton;
private OptionButton _backpackButton => CBackpackButton;
private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
private SingleMarkingPicker _hairPicker => CHairStylePicker;
private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
@@ -88,44 +65,39 @@ namespace Content.Client.Preferences.UI
private readonly Dictionary<string, BoxContainer> _jobCategories;
// Mildly hacky, as I don't trust prototype order to stay consistent and don't want the UI to break should a new one get added mid-edit. --moony
private readonly List<SpeciesPrototype> _speciesList;
private readonly List<AntagPreferenceSelector> _antagPreferences;
private readonly List<AntagPreferenceSelector> _antagPreferences = new();
private readonly List<TraitPreferenceSelector> _traitPreferences;
private SpriteView _previewSpriteView => CSpriteView;
private Button _previewRotateLeftButton => CSpriteRotateLeft;
private Button _previewRotateRightButton => CSpriteRotateRight;
private Direction _previewRotation = Direction.North;
private EntityUid? _previewDummy;
private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer;
private ColorSelectorSliders _rgbSkinColorSelector;
private bool _isDirty;
private bool _needUpdatePreview;
public int CharacterSlot;
public HumanoidCharacterProfile? Profile;
private MarkingSet _markingSet = new(); // storing this here feels iffy but a few things need it this high up
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager,
IEntityManager entityManager, IConfigurationManager configurationManager)
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IConfigurationManager configurationManager)
{
RobustXamlLoader.Load(this);
_prototypeManager = prototypeManager;
_entMan = entityManager;
_preferencesManager = preferencesManager;
_configurationManager = configurationManager;
_markingManager = IoCManager.Resolve<MarkingManager>();
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.PreviewDummyUpdated += OnDummyUpdate;
SpeciesInfoButton.ToolTip = Loc.GetString("humanoid-profile-editor-guidebook-button-tooltip");
_previewSpriteView.SetEntity(controller.GetPreviewDummy());
#region Left
#region Randomize
#endregion Randomize
#region Name
_nameEdit.OnTextChanged += args => { SetName(args.Text); };
@@ -139,8 +111,6 @@ namespace Content.Client.Preferences.UI
_tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
ShowClothes.OnPressed += ToggleClothes;
#region Sex
_sexButton.OnItemSelected += args =>
@@ -318,33 +288,6 @@ namespace Content.Client.Preferences.UI
#endregion Hair
#region Clothing
_clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit);
_clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt);
_clothingButton.OnItemSelected += args =>
{
_clothingButton.SelectId(args.Id);
SetClothing((ClothingPreference) args.Id);
};
#endregion Clothing
#region Backpack
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack);
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel);
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag);
_backpackButton.OnItemSelected += args =>
{
_backpackButton.SelectId(args.Id);
SetBackpack((BackpackPreference) args.Id);
};
#endregion Backpack
#region SpawnPriority
foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
@@ -399,40 +342,16 @@ namespace Content.Client.Preferences.UI
_jobPriorities = new List<JobPrioritySelector>();
_jobCategories = new Dictionary<string, BoxContainer>();
_requirements = IoCManager.Resolve<JobRequirementsManager>();
// TODO: Move this to the LobbyUIController instead of being spaghetti everywhere.
_requirements.Updated += UpdateAntagRequirements;
_requirements.Updated += UpdateRoleRequirements;
UpdateAntagRequirements();
UpdateRoleRequirements();
#endregion Jobs
#region Antags
_tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
_antagPreferences = new List<AntagPreferenceSelector>();
foreach (var antag in prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var selector = new AntagPreferenceSelector(antag);
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
if (selector.Disabled)
{
Profile = Profile?.WithAntagPreference(antag.ID, false);
IsDirty = true;
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithAntagPreference(antag.ID, preference);
IsDirty = true;
};
}
#endregion Antags
#region Traits
var traits = prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
@@ -483,7 +402,7 @@ namespace Content.Client.Preferences.UI
#region FlavorText
if (_configurationManager.GetCVar(CCVars.FlavorText))
if (configurationManager.GetCVar(CCVars.FlavorText))
{
var flavorText = new FlavorText.FlavorText();
_tabContainer.AddChild(flavorText);
@@ -500,22 +419,14 @@ namespace Content.Client.Preferences.UI
_previewRotateLeftButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCw();
_needUpdatePreview = true;
SetPreviewRotation(_previewRotation);
};
_previewRotateRightButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCcw();
_needUpdatePreview = true;
SetPreviewRotation(_previewRotation);
};
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy!.Value);
_previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
_previewSpriteView.SetEntity(_previewDummy);
#endregion Dummy
#endregion Left
@@ -538,22 +449,54 @@ namespace Content.Client.Preferences.UI
{
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = "Species";
var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
page = species;
if (_prototypeManager.TryIndex<GuideEntryPrototype>("Species", out var guideRoot))
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<string, GuideEntry>();
dict.Add("Species", guideRoot);
dict.Add(DefaultSpeciesGuidebook, guideRoot);
//TODO: Don't close the guidebook if its already open, just go to the correct page
guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
}
}
private void ToggleClothes(BaseButton.ButtonEventArgs obj)
private void OnDummyUpdate(EntityUid value)
{
RebuildSpriteView();
_previewSpriteView.SetEntity(value);
}
private void UpdateAntagRequirements()
{
_antagList.DisposeAllChildren();
_antagPreferences.Clear();
var btnGroup = new ButtonGroup();
foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var selector = new AntagPreferenceSelector(antag, btnGroup)
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
if (selector.Disabled)
{
Profile = Profile?.WithAntagPreference(antag.ID, false);
IsDirty = true;
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithAntagPreference(antag.ID, preference);
IsDirty = true;
};
}
}
private void UpdateRoleRequirements()
@@ -614,10 +557,16 @@ namespace Content.Client.Preferences.UI
.Where(job => job.SetPreference)
.ToArray();
Array.Sort(jobs, JobUIComparer.Instance);
var jobLoadoutGroup = new ButtonGroup();
foreach (var job in jobs)
{
var selector = new JobPrioritySelector(job, _prototypeManager);
RoleLoadout? loadout = null;
Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
if (!_requirements.IsAllowed(job, out var reason))
{
@@ -627,6 +576,13 @@ namespace Content.Client.Preferences.UI
category.AddChild(selector);
_jobPriorities.Add(selector);
selector.LoadoutUpdated += args =>
{
Profile?.SetLoadout(args);
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
IsDirty = true;
};
selector.PriorityChanged += priority =>
{
Profile = Profile?.WithJobPriority(job.ID, priority);
@@ -672,20 +628,10 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
_needUpdatePreview = true;
UpdatePreview();
IsDirty = true;
}
private void OnMarkingColorChange(List<Marking> markings)
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings));
IsDirty = true;
}
private void OnSkinColorOnValueChanged()
{
if (Profile is null) return;
@@ -745,33 +691,21 @@ namespace Content.Client.Preferences.UI
if (!disposing)
return;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy.Value);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.PreviewDummyUpdated -= OnDummyUpdate;
_requirements.Updated -= UpdateAntagRequirements;
_requirements.Updated -= UpdateRoleRequirements;
_preferencesManager.OnServerDataLoaded -= LoadServerData;
}
private void RebuildSpriteView()
{
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy!.Value);
_previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
_previewSpriteView.SetEntity(_previewDummy);
_needUpdatePreview = true;
}
private void LoadServerData()
{
Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
UpdateAntagRequirements();
UpdateRoleRequirements();
UpdateControls();
_needUpdatePreview = true;
}
private void SetAge(int newAge)
@@ -813,10 +747,9 @@ namespace Content.Client.Preferences.UI
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
UpdateSexControls(); // update sex for new species
RebuildSpriteView(); // they might have different inv so we need a new dummy
UpdateSpeciesGuidebookIcon();
IsDirty = true;
_needUpdatePreview = true;
UpdatePreview();
}
private void SetName(string newName)
@@ -825,18 +758,6 @@ namespace Content.Client.Preferences.UI
IsDirty = true;
}
private void SetClothing(ClothingPreference newClothing)
{
Profile = Profile?.WithClothingPreference(newClothing);
IsDirty = true;
}
private void SetBackpack(BackpackPreference newBackpack)
{
Profile = Profile?.WithBackpackPreference(newBackpack);
IsDirty = true;
}
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
{
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
@@ -847,12 +768,11 @@ namespace Content.Client.Preferences.UI
{
IsDirty = false;
if (Profile != null)
{
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
OnProfileChanged?.Invoke(Profile, CharacterSlot);
_needUpdatePreview = true;
}
if (Profile == null)
return;
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
OnProfileChanged?.Invoke(Profile, CharacterSlot);
}
private bool IsDirty
@@ -861,7 +781,6 @@ namespace Content.Client.Preferences.UI
set
{
_isDirty = value;
_needUpdatePreview = true;
UpdateSaveButton();
}
}
@@ -981,7 +900,7 @@ namespace Content.Client.Preferences.UI
if (!_prototypeManager.HasIndex<GuideEntryPrototype>(species))
return;
var style = speciesProto.GuideBookIcon;
const string style = "SpeciesInfoDefault";
SpeciesInfoButton.StyleClasses.Add(style);
}
@@ -1017,26 +936,6 @@ namespace Content.Client.Preferences.UI
_genderButton.SelectId((int) Profile.Gender);
}
private void UpdateClothingControls()
{
if (Profile == null)
{
return;
}
_clothingButton.SelectId((int) Profile.Clothing);
}
private void UpdateBackpackControls()
{
if (Profile == null)
{
return;
}
_backpackButton.SelectId((int) Profile.Backpack);
}
private void UpdateSpawnPriorityControls()
{
if (Profile == null)
@@ -1166,13 +1065,13 @@ namespace Content.Client.Preferences.UI
if (Profile is null)
return;
var humanoid = _entMan.System<HumanoidAppearanceSystem>();
humanoid.LoadProfile(_previewDummy!.Value, Profile);
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
SetPreviewRotation(_previewRotation);
}
if (ShowClothes.Pressed)
LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy!.Value, Profile);
_previewSpriteView.OverrideDirection = (Direction) ((int) _previewRotation % 4 * 2);
private void SetPreviewRotation(Direction direction)
{
_previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
}
public void UpdateControls()
@@ -1184,17 +1083,15 @@ namespace Content.Client.Preferences.UI
UpdateGenderControls();
UpdateSkinColor();
UpdateSpecies();
UpdateClothingControls();
UpdateBackpackControls();
UpdateSpawnPriorityControls();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateLoadouts();
UpdateJobPriorities();
UpdateAntagPreferences();
UpdateTraitPreferences();
UpdateMarkings();
RebuildSpriteView();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
@@ -1202,17 +1099,6 @@ namespace Content.Client.Preferences.UI
_preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_needUpdatePreview)
{
UpdatePreview();
_needUpdatePreview = false;
}
}
private void UpdateJobPriorities()
{
foreach (var prioritySelector in _jobPriorities)
@@ -1225,143 +1111,11 @@ namespace Content.Client.Preferences.UI
}
}
private abstract class RequirementsSelector<T> : Control
private void UpdateLoadouts()
{
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private StripeBack _lockStripe;
private Label _requirementsLabel;
protected RequirementsSelector(T proto)
foreach (var prioritySelector in _jobPriorities)
{
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
_requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
MouseFilter = MouseFilterMode.Stop,
Children =
{
_requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label()
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
var container = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
};
if (icon != null)
container.AddChild(icon);
container.AddChild(titleLabel);
container.AddChild(Options);
container.AddChild(_lockStripe);
AddChild(container);
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90
};
}
}
private sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan)
: base(proto)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
prioritySelector.CloseLoadout();
}
}
@@ -1386,41 +1140,6 @@ namespace Content.Client.Preferences.UI
}
}
private sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto)
: base(proto)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
Setup(items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}
private sealed class TraitPreferenceSelector : Control
{
public TraitPrototype Trait { get; }

View File

@@ -0,0 +1,46 @@
using System.Numerics;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
public sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(RoleLoadout? loadout, JobPrototype proto, ButtonGroup btnGroup, IPrototypeManager protoMan)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(loadout, items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
}
}

View File

@@ -0,0 +1,15 @@
<BoxContainer Name="Container" xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Orientation="Horizontal"
HorizontalExpand="True"
MouseFilter="Ignore"
Margin="0 0 0 5">
<Button Name="SelectButton" ToggleMode="True" Margin="0 0 5 0" HorizontalExpand="True"/>
<PanelContainer SetSize="64 64" HorizontalAlignment="Right">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<SpriteView Name="Sprite" Scale="4 4" MouseFilter="Stop"/>
</PanelContainer>
</BoxContainer>

View File

@@ -0,0 +1,74 @@
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutContainer : BoxContainer
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private readonly EntityUid? _entity;
public Button Select => SelectButton;
public LoadoutContainer(ProtoId<LoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SelectButton.Disabled = disabled;
if (disabled && reason != null)
{
var tooltip = new Tooltip();
tooltip.SetMessage(reason);
SelectButton.TooltipSupplier = _ => tooltip;
}
if (_protoManager.TryIndex(proto, out var loadProto))
{
var ent = _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
if (ent != null)
{
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
Sprite.TooltipSupplier = _ => spriteTooltip;
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(_entity);
}
public bool Pressed
{
get => SelectButton.Pressed;
set => SelectButton.Pressed = value;
}
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
}

View File

@@ -0,0 +1,10 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
</PanelContainer>
<!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
<Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
</BoxContainer>

View File

@@ -0,0 +1,93 @@
using System.Linq;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutGroupContainer : BoxContainer
{
private readonly LoadoutGroupPrototype _groupProto;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
_groupProto = groupProto;
RefreshLoadouts(loadout, session, collection);
}
/// <summary>
/// Updates button availabilities and buttons.
/// </summary>
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{
var protoMan = collection.Resolve<IPrototypeManager>();
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
RestrictionsContainer.DisposeAllChildren();
if (_groupProto.MinLimit > 0)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-min-limit", ("count", _groupProto.MinLimit)),
Margin = new Thickness(5, 0, 5, 5),
});
}
if (_groupProto.MaxLimit > 0)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-max-limit", ("count", _groupProto.MaxLimit)),
Margin = new Thickness(5, 0, 5, 5),
});
}
if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-points-limit", ("count", loadout.Points.Value), ("max", roleProto.Points.Value)),
Margin = new Thickness(5, 0, 5, 5),
});
}
LoadoutsContainer.DisposeAllChildren();
// Didn't use options because this is more robust in future.
var selected = loadout.SelectedLoadouts[_groupProto.ID];
foreach (var loadoutProto in _groupProto.Loadouts)
{
if (!protoMan.TryIndex(loadoutProto, out var loadProto))
continue;
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
var pressed = matchingLoadout != null;
var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason);
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
loadoutContainer.Select.Pressed = pressed;
loadoutContainer.Text = loadoutSystem.GetName(loadProto);
loadoutContainer.Select.OnPressed += args =>
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(loadoutProto);
else
OnLoadoutUnpressed?.Invoke(loadoutProto);
};
LoadoutsContainer.AddChild(loadoutContainer);
}
}
}

View File

@@ -0,0 +1,10 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="800 800"
MinSize="800 64">
<VerticalTabContainer Name="LoadoutGroupsContainer"
VerticalExpand="True"
HorizontalExpand="True">
</VerticalTabContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,60 @@
using Content.Client.Lobby;
using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutWindow : FancyWindow
{
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
private List<LoadoutGroupContainer> _groups = new();
public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
var protoManager = collection.Resolve<IPrototypeManager>();
foreach (var group in proto.Groups)
{
if (!protoManager.TryIndex(group, out var groupProto))
continue;
var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection);
LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
_groups.Add(container);
container.OnLoadoutPressed += args =>
{
OnLoadoutPressed?.Invoke(group, args);
};
container.OnLoadoutUnpressed += args =>
{
OnLoadoutUnpressed?.Invoke(group, args);
};
}
}
public override void Close()
{
base.Close();
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(null);
}
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{
foreach (var group in _groups)
{
group.RefreshLoadouts(loadout, session, collection);
}
}
}

View File

@@ -0,0 +1,222 @@
using System.Numerics;
using Content.Client.Lobby;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI;
public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototype
{
private ButtonGroup _loadoutGroup;
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private readonly StripeBack _lockStripe;
private LoadoutWindow? _loadoutWindow;
private RoleLoadout? _loadout;
/// <summary>
/// Raised if a loadout has been updated.
/// </summary>
public event Action<RoleLoadout>? LoadoutUpdated;
protected RequirementsSelector(T proto, ButtonGroup loadoutGroup)
{
_loadoutGroup = loadoutGroup;
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft,
HorizontalExpand = true,
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
var requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
HasMargins = false,
MouseFilter = MouseFilterMode.Stop,
Children =
{
requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup(RoleLoadout? loadout, (string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
_loadout = loadout;
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label()
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
if (icon != null)
AddChild(icon);
AddChild(titleLabel);
AddChild(Options);
AddChild(_lockStripe);
var loadoutWindowBtn = new Button()
{
Text = Loc.GetString("loadout-window"),
HorizontalAlignment = HAlignment.Right,
Group = _loadoutGroup,
Margin = new Thickness(3f, 0f, 0f, 0f),
};
var collection = IoCManager.Instance!;
var protoManager = collection.Resolve<IPrototypeManager>();
// If no loadout found then disabled button
if (!protoManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(Proto.ID)))
{
loadoutWindowBtn.Disabled = true;
}
// else
else
{
var session = collection.Resolve<IPlayerManager>().LocalSession!;
// TODO: Most of lobby state should be a uicontroller
// trying to handle all this shit is a big-ass mess.
// Every time I touch it I try to make it slightly better but it needs a howitzer dropped on it.
loadoutWindowBtn.OnPressed += args =>
{
if (args.Button.Pressed)
{
// We only create a loadout when necessary to avoid unnecessary DB entries.
_loadout ??= new RoleLoadout(LoadoutSystem.GetJobPrototype(Proto.ID));
_loadout.SetDefault(protoManager);
_loadoutWindow = new LoadoutWindow(_loadout, protoManager.Index(_loadout.Role), session, collection)
{
Title = Loc.GetString(Proto.ID + "-loadout"),
};
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
// If it's a job preview then refresh it.
if (Proto is JobPrototype jobProto)
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(jobProto);
}
_loadoutWindow.OnLoadoutUnpressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.RemoveLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateCharacterUI();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OnLoadoutPressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.AddLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateCharacterUI();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OpenCenteredLeft();
_loadoutWindow.OnClose += () =>
{
loadoutWindowBtn.Pressed = false;
_loadoutWindow?.Dispose();
_loadoutWindow = null;
};
}
else
{
CloseLoadout();
}
};
}
AddChild(loadoutWindowBtn);
}
public void CloseLoadout()
{
_loadoutWindow?.Close();
_loadoutWindow?.Dispose();
_loadoutWindow = null;
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90,
HorizontalExpand = true,
};
}
}

View File

@@ -19,7 +19,7 @@ public sealed partial class MindTests
await using var pair = await PoolManager.GetServerClient(settings);
// Client is connected with a valid entity & mind
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
// Delete **everything**
@@ -28,6 +28,12 @@ public sealed partial class MindTests
await pair.RunTicksSync(5);
Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
foreach (var ent in pair.Client.EntMan.GetEntities())
{
Console.WriteLine(pair.Client.EntMan.ToPrettyString(ent));
}
Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
// Create a new map.
@@ -36,7 +42,7 @@ public sealed partial class MindTests
await pair.RunTicksSync(5);
// Client is not attached to anything
Assert.That(pair.Client.Player?.ControlledEntity, Is.Null);
Assert.That(pair.Client.AttachedEntity, Is.Null);
Assert.That(pair.PlayerData?.Mind, Is.Null);
// Attempt to ghost
@@ -45,9 +51,9 @@ public sealed partial class MindTests
await pair.RunTicksSync(10);
// Client should be attached to a ghost placed on the new map.
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value);
Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
await pair.CleanReturnAsync();

View File

@@ -0,0 +1,44 @@
using Content.Server.Station.Systems;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Preferences;
[TestFixture]
[Ignore("HumanoidAppearance crashes upon loading default profiles.")]
public sealed class LoadoutTests
{
/// <summary>
/// Checks that an empty loadout still spawns with default gear and not naked.
/// </summary>
[Test]
public async Task TestEmptyLoadout()
{
var pair = await PoolManager.GetServerClient(new PoolSettings()
{
Dirty = true,
});
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
// Check that an empty role loadout spawns gear
var stationSystem = entManager.System<StationSpawningSystem>();
var testMap = await pair.CreateTestMap();
// That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing.
var profile = new HumanoidCharacterProfile();
profile.SetLoadout(new RoleLoadout("TestRoleLoadout"));
stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
{
// Sue me, there's so much involved in setting up jobs
Prototype = "CargoTechnician"
}, profile, station: null);
await pair.CleanReturnAsync();
}
}

View File

@@ -4,6 +4,8 @@ using Content.Server.Database;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
@@ -53,8 +55,6 @@ namespace Content.IntegrationTests.Tests.Preferences
Color.Beige,
new ()
),
ClothingPreference.Jumpskirt,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@@ -62,7 +62,8 @@ namespace Content.IntegrationTests.Tests.Preferences
},
PreferenceUnavailableMode.StayInLobby,
new List<string> (),
new List<string>()
new List<string>(),
new Dictionary<string, RoleLoadout>()
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ClothingRemoval : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backpack",
table: "profile");
migrationBuilder.DropColumn(
name: "clothing",
table: "profile");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "backpack",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "clothing",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class Loadouts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "profile_role_loadout",
columns: table => new
{
profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_id = table.Column<int>(type: "integer", nullable: false),
role_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
table.ForeignKey(
name: "FK_profile_role_loadout_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout_group",
columns: table => new
{
profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false),
group_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
table.ForeignKey(
name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loa~",
column: x => x.profile_role_loadout_id,
principalTable: "profile_role_loadout",
principalColumn: "profile_role_loadout_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout",
columns: table => new
{
profile_loadout_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false),
loadout_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
table.ForeignKey(
name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group~",
column: x => x.profile_loadout_group_id,
principalTable: "profile_loadout_group",
principalColumn: "profile_loadout_group_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_profile_loadout_group_id",
table: "profile_loadout",
column: "profile_loadout_group_id");
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_group_profile_role_loadout_id",
table: "profile_loadout_group",
column: "profile_role_loadout_id");
migrationBuilder.CreateIndex(
name: "IX_profile_role_loadout_profile_id",
table: "profile_role_loadout",
column: "profile_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "profile_loadout");
migrationBuilder.DropTable(
name: "profile_loadout_group");
migrationBuilder.DropTable(
name: "profile_role_loadout");
}
}
}

View File

@@ -735,21 +735,11 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer")
.HasColumnName("age");
b.Property<string>("Backpack")
.IsRequired()
.HasColumnType("text")
.HasColumnName("backpack");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("char_name");
b.Property<string>("Clothing")
.IsRequired()
.HasColumnType("text")
.HasColumnName("clothing");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnType("text")
@@ -832,6 +822,84 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_loadout_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("LoadoutName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("loadout_name");
b.Property<int>("ProfileLoadoutGroupId")
.HasColumnType("integer")
.HasColumnName("profile_loadout_group_id");
b.HasKey("Id")
.HasName("PK_profile_loadout");
b.HasIndex("ProfileLoadoutGroupId");
b.ToTable("profile_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_loadout_group_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("GroupName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("group_name");
b.Property<int>("ProfileRoleLoadoutId")
.HasColumnType("integer")
.HasColumnName("profile_role_loadout_id");
b.HasKey("Id")
.HasName("PK_profile_loadout_group");
b.HasIndex("ProfileRoleLoadoutId");
b.ToTable("profile_loadout_group", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_role_loadout_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ProfileId")
.HasColumnType("integer")
.HasColumnName("profile_id");
b.Property<string>("RoleName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("role_name");
b.HasKey("Id")
.HasName("PK_profile_role_loadout");
b.HasIndex("ProfileId");
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@@ -1519,6 +1587,42 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
.WithMany("Loadouts")
.HasForeignKey("ProfileLoadoutGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
b.Navigation("ProfileLoadoutGroup");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
.WithMany("Groups")
.HasForeignKey("ProfileRoleLoadoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
b.Navigation("ProfileRoleLoadout");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Loadouts")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_role_loadout_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@@ -1731,9 +1835,21 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Jobs");
b.Navigation("Loadouts");
b.Navigation("Traits");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Navigation("Loadouts");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ClothingRemoval : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backpack",
table: "profile");
migrationBuilder.DropColumn(
name: "clothing",
table: "profile");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "backpack",
table: "profile",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "clothing",
table: "profile",
type: "TEXT",
nullable: false,
defaultValue: "");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class Loadouts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "profile_role_loadout",
columns: table => new
{
profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_id = table.Column<int>(type: "INTEGER", nullable: false),
role_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
table.ForeignKey(
name: "FK_profile_role_loadout_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout_group",
columns: table => new
{
profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false),
group_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
table.ForeignKey(
name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id",
column: x => x.profile_role_loadout_id,
principalTable: "profile_role_loadout",
principalColumn: "profile_role_loadout_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout",
columns: table => new
{
profile_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false),
loadout_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
table.ForeignKey(
name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group_id",
column: x => x.profile_loadout_group_id,
principalTable: "profile_loadout_group",
principalColumn: "profile_loadout_group_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_profile_loadout_group_id",
table: "profile_loadout",
column: "profile_loadout_group_id");
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_group_profile_role_loadout_id",
table: "profile_loadout_group",
column: "profile_role_loadout_id");
migrationBuilder.CreateIndex(
name: "IX_profile_role_loadout_profile_id",
table: "profile_role_loadout",
column: "profile_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "profile_loadout");
migrationBuilder.DropTable(
name: "profile_loadout_group");
migrationBuilder.DropTable(
name: "profile_role_loadout");
}
}
}

View File

@@ -688,21 +688,11 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("age");
b.Property<string>("Backpack")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("backpack");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("char_name");
b.Property<string>("Clothing")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("clothing");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnType("TEXT")
@@ -785,6 +775,78 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_id");
b.Property<string>("LoadoutName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("loadout_name");
b.Property<int>("ProfileLoadoutGroupId")
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_group_id");
b.HasKey("Id")
.HasName("PK_profile_loadout");
b.HasIndex("ProfileLoadoutGroupId");
b.ToTable("profile_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_group_id");
b.Property<string>("GroupName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("group_name");
b.Property<int>("ProfileRoleLoadoutId")
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.HasKey("Id")
.HasName("PK_profile_loadout_group");
b.HasIndex("ProfileRoleLoadoutId");
b.ToTable("profile_loadout_group", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER")
.HasColumnName("profile_id");
b.Property<string>("RoleName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("role_name");
b.HasKey("Id")
.HasName("PK_profile_role_loadout");
b.HasIndex("ProfileId");
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@@ -1450,6 +1512,42 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
.WithMany("Loadouts")
.HasForeignKey("ProfileLoadoutGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
b.Navigation("ProfileLoadoutGroup");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
.WithMany("Groups")
.HasForeignKey("ProfileRoleLoadoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
b.Navigation("ProfileRoleLoadout");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Loadouts")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_role_loadout_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@@ -1662,9 +1760,21 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Jobs");
b.Navigation("Loadouts");
b.Navigation("Traits");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Navigation("Loadouts");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");

View File

@@ -56,8 +56,26 @@ namespace Content.Server.Database
.IsUnique();
modelBuilder.Entity<Trait>()
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
.IsUnique();
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
.IsUnique();
modelBuilder.Entity<ProfileRoleLoadout>()
.HasOne(e => e.Profile)
.WithMany(e => e.Loadouts)
.HasForeignKey(e => e.ProfileId)
.IsRequired();
modelBuilder.Entity<ProfileLoadoutGroup>()
.HasOne(e => e.ProfileRoleLoadout)
.WithMany(e => e.Groups)
.HasForeignKey(e => e.ProfileRoleLoadoutId)
.IsRequired();
modelBuilder.Entity<ProfileLoadout>()
.HasOne(e => e.ProfileLoadoutGroup)
.WithMany(e => e.Loadouts)
.HasForeignKey(e => e.ProfileLoadoutGroupId)
.IsRequired();
modelBuilder.Entity<Job>()
.HasIndex(j => j.ProfileId);
@@ -337,13 +355,13 @@ namespace Content.Server.Database
public string FacialHairColor { get; set; } = null!;
public string EyeColor { get; set; } = null!;
public string SkinColor { get; set; } = null!;
public string Clothing { get; set; } = null!;
public string Backpack { get; set; } = null!;
public int SpawnPriority { get; set; } = 0;
public List<Job> Jobs { get; } = new();
public List<Antag> Antags { get; } = new();
public List<Trait> Traits { get; } = new();
public List<ProfileRoleLoadout> Loadouts { get; } = new();
[Column("pref_unavailable")] public DbPreferenceUnavailableMode PreferenceUnavailable { get; set; }
public int PreferenceId { get; set; }
@@ -387,6 +405,79 @@ namespace Content.Server.Database
public string TraitName { get; set; } = null!;
}
#region Loadouts
/// <summary>
/// Corresponds to a single role's loadout inside the DB.
/// </summary>
public class ProfileRoleLoadout
{
public int Id { get; set; }
public int ProfileId { get; set; }
public Profile Profile { get; set; } = null!;
/// <summary>
/// The corresponding role prototype on the profile.
/// </summary>
public string RoleName { get; set; } = string.Empty;
/// <summary>
/// Store the saved loadout groups. These may get validated and removed when loaded at runtime.
/// </summary>
public List<ProfileLoadoutGroup> Groups { get; set; } = new();
}
/// <summary>
/// Corresponds to a loadout group prototype with the specified loadouts attached.
/// </summary>
public class ProfileLoadoutGroup
{
public int Id { get; set; }
public int ProfileRoleLoadoutId { get; set; }
/// <summary>
/// The corresponding RoleLoadout that owns this.
/// </summary>
public ProfileRoleLoadout ProfileRoleLoadout { get; set; } = null!;
/// <summary>
/// The corresponding group prototype.
/// </summary>
public string GroupName { get; set; } = string.Empty;
/// <summary>
/// Selected loadout prototype. Null if none is set.
/// May get validated at runtime and updated to to the default.
/// </summary>
public List<ProfileLoadout> Loadouts { get; set; } = new();
}
/// <summary>
/// Corresponds to a selected loadout.
/// </summary>
public class ProfileLoadout
{
public int Id { get; set; }
public int ProfileLoadoutGroupId { get; set; }
public ProfileLoadoutGroup ProfileLoadoutGroup { get; set; } = null!;
/// <summary>
/// Corresponding loadout prototype.
/// </summary>
public string LoadoutName { get; set; } = string.Empty;
/*
* Insert extra data here like custom descriptions or colors or whatever.
*/
}
#endregion
public enum DbPreferenceUnavailableMode
{
// These enum values HAVE to match the ones in PreferenceUnavailableMode in Shared.

View File

@@ -97,7 +97,7 @@ namespace Content.Server.Administration.Commands
foreach (var slot in slots)
{
invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = startingGear.GetGear(slot.Name, profile);
var gearStr = startingGear.GetGear(slot.Name);
if (gearStr == string.Empty)
{
continue;

View File

@@ -13,6 +13,8 @@ using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums;
using Robust.Shared.Network;
@@ -40,6 +42,10 @@ namespace Content.Server.Database
.Include(p => p.Profiles).ThenInclude(h => h.Jobs)
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
.Include(p => p.Profiles).ThenInclude(h => h.Traits)
.Include(p => p.Profiles)
.ThenInclude(h => h.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSingleQuery()
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
@@ -88,6 +94,9 @@ namespace Content.Server.Database
.Include(p => p.Jobs)
.Include(p => p.Antags)
.Include(p => p.Traits)
.Include(p => p.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSplitQuery()
.SingleOrDefault(h => h.Slot == slot);
@@ -179,14 +188,6 @@ namespace Content.Server.Database
if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
sex = sexVal;
var clothing = ClothingPreference.Jumpsuit;
if (Enum.TryParse<ClothingPreference>(profile.Clothing, true, out var clothingVal))
clothing = clothingVal;
var backpack = BackpackPreference.Backpack;
if (Enum.TryParse<BackpackPreference>(profile.Backpack, true, out var backpackVal))
backpack = backpackVal;
var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
@@ -209,6 +210,27 @@ namespace Content.Server.Database
}
}
var loadouts = new Dictionary<string, RoleLoadout>();
foreach (var role in profile.Loadouts)
{
var loadout = new RoleLoadout(role.RoleName);
foreach (var group in role.Groups)
{
var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
foreach (var profLoadout in group.Loadouts)
{
groupLoadouts.Add(new Loadout()
{
Prototype = profLoadout.LoadoutName,
});
}
}
loadouts[role.RoleName] = loadout;
}
return new HumanoidCharacterProfile(
profile.CharacterName,
profile.FlavorText,
@@ -226,13 +248,12 @@ namespace Content.Server.Database
Color.FromHex(profile.SkinColor),
markings
),
clothing,
backpack,
spawnPriority,
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToList(),
traits.ToList()
traits.ToList(),
loadouts
);
}
@@ -259,8 +280,6 @@ namespace Content.Server.Database
profile.FacialHairColor = appearance.FacialHairColor.ToHex();
profile.EyeColor = appearance.EyeColor.ToHex();
profile.SkinColor = appearance.SkinColor.ToHex();
profile.Clothing = humanoid.Clothing.ToString();
profile.Backpack = humanoid.Backpack.ToString();
profile.SpawnPriority = (int) humanoid.SpawnPriority;
profile.Markings = markings;
profile.Slot = slot;
@@ -285,6 +304,36 @@ namespace Content.Server.Database
.Select(t => new Trait {TraitName = t})
);
profile.Loadouts.Clear();
foreach (var (role, loadouts) in humanoid.Loadouts)
{
var dz = new ProfileRoleLoadout()
{
RoleName = role,
};
foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
{
var profileGroup = new ProfileLoadoutGroup()
{
GroupName = group,
};
foreach (var loadout in groupLoadouts)
{
profileGroup.Loadouts.Add(new ProfileLoadout()
{
LoadoutName = loadout.Prototype,
});
}
dz.Groups.Add(profileGroup);
}
profile.Loadouts.Add(dz);
}
return profile;
}
#endregion

View File

@@ -709,7 +709,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
_humanoid.LoadProfile(mob, profile);
var gear = _prototypeManager.Index(spawnDetails.GearProto);
_stationSpawning.EquipStartingGear(mob, gear, profile);
_stationSpawning.EquipStartingGear(mob, gear);
_npcFaction.RemoveFaction(mob, "NanoTrasen", false);
_npcFaction.AddFaction(mob, "Syndicate");

View File

@@ -249,7 +249,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
_mindSystem.TransferTo(newMind, mob);
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
_stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
_stationSpawningSystem.EquipStartingGear(mob, pirateGear);
_npcFaction.RemoveFaction(mob, EnemyFactionId, false);
_npcFaction.AddFaction(mob, PirateFactionId);

View File

@@ -22,6 +22,7 @@ using Content.Server.Worldgen.Tools;
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Managers;
using Content.Shared.Kitchen;
using Content.Shared.Players.PlayTimeTracking;
namespace Content.Server.IoC
{
@@ -58,6 +59,7 @@ namespace Content.Server.IoC
IoCManager.Register<PoissonDiskSampler>();
IoCManager.Register<DiscordWebhook>();
IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
}
}

View File

@@ -54,7 +54,7 @@ public delegate void CalcPlayTimeTrackersCallback(ICommonSession player, HashSet
/// Operations like refreshing and sending play time info to clients are deferred until the next frame (note: not tick).
/// </para>
/// </remarks>
public sealed class PlayTimeTrackingManager
public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IServerNetManager _net = default!;
@@ -201,6 +201,11 @@ public sealed class PlayTimeTrackingManager
}
}
public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
{
return GetTrackerTimes(session);
}
private void SendPlayTimes(ICommonSession pSession)
{
var roles = GetTrackerTimes(pSession);

View File

@@ -8,6 +8,7 @@ using Content.Shared.CCVar;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -25,6 +26,7 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _protos = default!;
// Cache player prefs on the server so we don't need as much async hell related to them.
@@ -98,8 +100,10 @@ namespace Content.Server.Preferences.Managers
}
var curPrefs = prefsData.Prefs!;
var session = _playerManager.GetSessionById(userId);
var collection = IoCManager.Instance!;
profile.EnsureValid(_cfg, _protos);
profile.EnsureValid(session, collection);
var profiles = new Dictionary<int, ICharacterProfile>(curPrefs.Characters)
{
@@ -260,17 +264,20 @@ namespace Content.Server.Preferences.Managers
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random());
}
return SanitizePreferences(prefs);
var session = _playerManager.GetSessionById(userId);
var collection = IoCManager.Instance!;
return SanitizePreferences(session, prefs, collection);
}
private PlayerPreferences SanitizePreferences(PlayerPreferences prefs)
private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPreferences prefs, IDependencyCollection collection)
{
// Clean up preferences in case of changes to the game,
// such as removed jobs still being selected.
return new PlayerPreferences(prefs.Characters.Select(p =>
{
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(_cfg, _protos));
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(session, collection));
}), prefs.SelectedCharacterIndex, prefs.AdminOOCColor);
}

View File

@@ -51,7 +51,7 @@ public sealed class SpawnPointSystem : EntitySystem
// TODO: Refactor gameticker spawning code so we don't have to do this!
var points2 = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
if (points2.MoveNext(out var uid, out var spawnPoint, out var xform))
if (points2.MoveNext(out var spawnPoint, out var xform))
{
possiblePositions.Add(xform.Coordinates);
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Access.Systems;
using Content.Server.DetailExaminable;
using Content.Server.Humanoid;
@@ -10,10 +11,12 @@ using Content.Server.Station.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
@@ -86,7 +89,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
if (station != null && profile != null)
{
/// Try to call the character's preferred spawner first.
// Try to call the character's preferred spawner first.
if (_spawnerCallbacks.TryGetValue(profile.SpawnPriority, out var preferredSpawner))
{
preferredSpawner(ev);
@@ -101,9 +104,11 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
}
else
{
/// Call all of them in the typical order.
// Call all of them in the typical order.
foreach (var typicalSpawner in _spawnerCallbacks.Values)
{
typicalSpawner(ev);
}
}
}
@@ -134,7 +139,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
EntityUid? station,
EntityUid? entity = null)
{
_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out JobPrototype? prototype);
_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var prototype);
// If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff.
if (prototype?.JobEntity != null)
@@ -176,11 +181,38 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
if (prototype?.StartingGear != null)
{
var startingGear = _prototypeManager.Index<StartingGearPrototype>(prototype.StartingGear);
EquipStartingGear(entity.Value, startingGear, profile);
EquipStartingGear(entity.Value, startingGear);
if (profile != null)
EquipIdCard(entity.Value, profile.Name, prototype, station);
}
// Run loadouts after so stuff like storage loadouts can get
var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? loadoutProto))
{
RoleLoadout? loadout = null;
profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
// Set to default if not present
if (loadout == null)
{
loadout = new RoleLoadout(jobLoadout);
loadout.SetDefault(_prototypeManager);
}
// Order loadout selections by the order they appear on the prototype.
foreach (var group in loadout.SelectedLoadouts.OrderBy(x => loadoutProto.Groups.FindIndex(e => e == x.Key)))
{
foreach (var items in group.Value)
{
// Handle any extra data here.
var startingGear = _prototypeManager.Index<StartingGearPrototype>(items.Prototype);
EquipStartingGear(entity.Value, startingGear);
}
}
}
if (profile != null)
{
_humanoidSystem.LoadProfile(entity.Value, profile);

View File

@@ -1,4 +1,6 @@
using System.Linq;
using Content.Shared.Clothing.Components;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Prototypes;
@@ -24,12 +26,94 @@ public sealed class LoadoutSystem : EntitySystem
SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit);
}
public static string GetJobPrototype(string? loadout)
{
if (string.IsNullOrEmpty(loadout))
return string.Empty;
return "Job" + loadout;
}
/// <summary>
/// Tries to get the first entity prototype for operations such as sprite drawing.
/// </summary>
public EntProtoId? GetFirstOrNull(LoadoutPrototype loadout)
{
if (!_protoMan.TryIndex(loadout.Equipment, out var gear))
return null;
var count = gear.Equipment.Count + gear.Inhand.Count + gear.Storage.Values.Sum(x => x.Count);
if (count == 1)
{
if (gear.Equipment.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Equipment.Values.First(), out var proto))
{
return proto.ID;
}
if (gear.Inhand.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Inhand[0], out proto))
{
return proto.ID;
}
// Storage moment
foreach (var ents in gear.Storage.Values)
{
foreach (var ent in ents)
{
return ent;
}
}
}
return null;
}
/// <summary>
/// Tries to get the name of a loadout.
/// </summary>
public string GetName(LoadoutPrototype loadout)
{
if (!_protoMan.TryIndex(loadout.Equipment, out var gear))
return Loc.GetString("loadout-unknown");
var count = gear.Equipment.Count + gear.Storage.Values.Sum(o => o.Count) + gear.Inhand.Count;
if (count == 1)
{
if (gear.Equipment.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Equipment.Values.First(), out var proto))
{
return proto.Name;
}
if (gear.Inhand.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Inhand[0], out proto))
{
return proto.Name;
}
foreach (var values in gear.Storage.Values)
{
if (values.Count != 1)
continue;
if (_protoMan.TryIndex<EntityPrototype>(values[0], out proto))
{
return proto.Name;
}
break;
}
}
return Loc.GetString($"loadout-{loadout.ID}");
}
private void OnMapInit(EntityUid uid, LoadoutComponent component, MapInitEvent args)
{
if (component.Prototypes == null)
return;
var proto = _protoMan.Index<StartingGearPrototype>(_random.Pick(component.Prototypes));
_station.EquipStartingGear(uid, proto, null);
_station.EquipStartingGear(uid, proto);
}
}

View File

@@ -66,14 +66,14 @@ public sealed partial class SpeciesPrototype : IPrototype
/// <summary>
/// Humanoid species variant used by this entity.
/// </summary>
[DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype { get; private set; } = default!;
[DataField(required: true)]
public EntProtoId Prototype { get; private set; } = default!;
/// <summary>
/// Prototype used by the species for the dress-up doll in various menus.
/// </summary>
[DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string DollPrototype { get; private set; } = default!;
[DataField(required: true)]
public EntProtoId DollPrototype { get; private set; } = default!;
/// <summary>
/// Method of skin coloration used by the species.
@@ -120,12 +120,6 @@ public sealed partial class SpeciesPrototype : IPrototype
/// </summary>
[DataField]
public int MaxAge = 120;
/// <summary>
/// The Style used for the guidebook info link in the character profile editor
/// </summary>
[DataField]
public string GuideBookIcon = "SpeciesInfoDefault";
}
public enum SpeciesNaming : byte

View File

@@ -0,0 +1,12 @@
using Robust.Shared.Player;
namespace Content.Shared.Players.PlayTimeTracking;
public interface ISharedPlaytimeManager
{
/// <summary>
/// Gets the playtimes for the session or an empty dictionary if none found.
/// </summary>
IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session);
}

View File

@@ -1,12 +0,0 @@
namespace Content.Shared.Preferences
{
/// <summary>
/// The backpack preference for a profile. Stored in database!
/// </summary>
public enum BackpackPreference
{
Backpack,
Satchel,
Duffelbag
}
}

View File

@@ -1,11 +0,0 @@
namespace Content.Shared.Preferences
{
/// <summary>
/// The clothing preference for a profile. Stored in database!
/// </summary>
public enum ClothingPreference
{
Jumpsuit,
Jumpskirt
}
}

View File

@@ -1,15 +1,17 @@
using System.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Random.Helpers;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.Traits;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
@@ -31,6 +33,11 @@ namespace Content.Shared.Preferences
private readonly List<string> _antagPreferences;
private readonly List<string> _traitPreferences;
public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
private Dictionary<string, RoleLoadout> _loadouts;
// What in the lord is happening here.
private HumanoidCharacterProfile(
string name,
string flavortext,
@@ -39,13 +46,12 @@ namespace Content.Shared.Preferences
Sex sex,
Gender gender,
HumanoidCharacterAppearance appearance,
ClothingPreference clothing,
BackpackPreference backpack,
SpawnPriorityPreference spawnPriority,
Dictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
List<string> antagPreferences,
List<string> traitPreferences)
List<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
{
Name = name;
FlavorText = flavortext;
@@ -54,13 +60,12 @@ namespace Content.Shared.Preferences
Sex = sex;
Gender = gender;
Appearance = appearance;
Clothing = clothing;
Backpack = backpack;
SpawnPriority = spawnPriority;
_jobPriorities = jobPriorities;
PreferenceUnavailable = preferenceUnavailable;
_antagPreferences = antagPreferences;
_traitPreferences = traitPreferences;
_loadouts = loadouts;
}
/// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
@@ -68,15 +73,16 @@ namespace Content.Shared.Preferences
HumanoidCharacterProfile other,
Dictionary<string, JobPriority> jobPriorities,
List<string> antagPreferences,
List<string> traitPreferences)
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack, other.SpawnPriority,
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences)
List<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.SpawnPriority,
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, loadouts)
{
}
/// <summary>Copy constructor</summary>
private HumanoidCharacterProfile(HumanoidCharacterProfile other)
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences))
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts))
{
}
@@ -88,15 +94,14 @@ namespace Content.Shared.Preferences
Sex sex,
Gender gender,
HumanoidCharacterAppearance appearance,
ClothingPreference clothing,
BackpackPreference backpack,
SpawnPriorityPreference spawnPriority,
IReadOnlyDictionary<string, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
IReadOnlyList<string> antagPreferences,
IReadOnlyList<string> traitPreferences)
: this(name, flavortext, species, age, sex, gender, appearance, clothing, backpack, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences))
IReadOnlyList<string> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
: this(name, flavortext, species, age, sex, gender, appearance, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences), new Dictionary<string, RoleLoadout>(loadouts))
{
}
@@ -113,8 +118,6 @@ namespace Content.Shared.Preferences
Sex.Male,
Gender.Male,
new HumanoidCharacterAppearance(),
ClothingPreference.Jumpsuit,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@@ -122,7 +125,8 @@ namespace Content.Shared.Preferences
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>())
new List<string>(),
new Dictionary<string, RoleLoadout>())
{
}
@@ -141,8 +145,6 @@ namespace Content.Shared.Preferences
Sex.Male,
Gender.Male,
HumanoidCharacterAppearance.DefaultWithSpecies(species),
ClothingPreference.Jumpsuit,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@@ -150,7 +152,8 @@ namespace Content.Shared.Preferences
},
PreferenceUnavailableMode.SpawnAsOverflow,
new List<string>(),
new List<string>());
new List<string>(),
new Dictionary<string, RoleLoadout>());
}
// TODO: This should eventually not be a visual change only.
@@ -195,11 +198,11 @@ namespace Content.Shared.Preferences
var name = GetName(species, gender);
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, SpawnPriorityPreference.None,
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
{SharedGameTicker.FallbackOverflowJob, JobPriority.High},
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>());
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>());
}
public string Name { get; private set; }
@@ -219,8 +222,6 @@ namespace Content.Shared.Preferences
[DataField("appearance")]
public HumanoidCharacterAppearance Appearance { get; private set; }
public ClothingPreference Clothing { get; private set; }
public BackpackPreference Backpack { get; private set; }
public SpawnPriorityPreference SpawnPriority { get; private set; }
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
public IReadOnlyList<string> AntagPreferences => _antagPreferences;
@@ -263,21 +264,14 @@ namespace Content.Shared.Preferences
return new(this) { Appearance = appearance };
}
public HumanoidCharacterProfile WithClothingPreference(ClothingPreference clothing)
{
return new(this) { Clothing = clothing };
}
public HumanoidCharacterProfile WithBackpackPreference(BackpackPreference backpack)
{
return new(this) { Backpack = backpack };
}
public HumanoidCharacterProfile WithSpawnPriorityPreference(SpawnPriorityPreference spawnPriority)
{
return new(this) { SpawnPriority = spawnPriority };
}
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
{
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences);
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
@@ -291,7 +285,7 @@ namespace Content.Shared.Preferences
{
dictionary[jobId] = priority;
}
return new(this, dictionary, _antagPreferences, _traitPreferences);
return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
@@ -301,7 +295,7 @@ namespace Content.Shared.Preferences
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
{
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences);
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
@@ -321,7 +315,7 @@ namespace Content.Shared.Preferences
list.Remove(antagId);
}
}
return new(this, _jobPriorities, list, _traitPreferences);
return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
}
public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
@@ -343,7 +337,7 @@ namespace Content.Shared.Preferences
list.Remove(traitId);
}
}
return new(this, _jobPriorities, _antagPreferences, list);
return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
}
public string Summary =>
@@ -362,17 +356,19 @@ namespace Content.Shared.Preferences
if (Sex != other.Sex) return false;
if (Gender != other.Gender) return false;
if (PreferenceUnavailable != other.PreferenceUnavailable) return false;
if (Clothing != other.Clothing) return false;
if (Backpack != other.Backpack) return false;
if (SpawnPriority != other.SpawnPriority) return false;
if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false;
if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false;
if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
return Appearance.MemberwiseEquals(other.Appearance);
}
public void EnsureValid(IConfigurationManager configManager, IPrototypeManager prototypeManager)
public void EnsureValid(ICommonSession session, IDependencyCollection collection)
{
var configManager = collection.Resolve<IConfigurationManager>();
var prototypeManager = collection.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex<SpeciesPrototype>(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
{
Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
@@ -455,21 +451,6 @@ namespace Content.Shared.Preferences
_ => PreferenceUnavailableMode.StayInLobby // Invalid enum values.
};
var clothing = Clothing switch
{
ClothingPreference.Jumpsuit => ClothingPreference.Jumpsuit,
ClothingPreference.Jumpskirt => ClothingPreference.Jumpskirt,
_ => ClothingPreference.Jumpsuit // Invalid enum values.
};
var backpack = Backpack switch
{
BackpackPreference.Backpack => BackpackPreference.Backpack,
BackpackPreference.Satchel => BackpackPreference.Satchel,
BackpackPreference.Duffelbag => BackpackPreference.Duffelbag,
_ => BackpackPreference.Backpack // Invalid enum values.
};
var spawnPriority = SpawnPriority switch
{
SpawnPriorityPreference.None => SpawnPriorityPreference.None,
@@ -502,8 +483,6 @@ namespace Content.Shared.Preferences
Sex = sex;
Gender = gender;
Appearance = appearance;
Clothing = clothing;
Backpack = backpack;
SpawnPriority = spawnPriority;
_jobPriorities.Clear();
@@ -520,12 +499,31 @@ namespace Content.Shared.Preferences
_traitPreferences.Clear();
_traitPreferences.AddRange(traits);
// Checks prototypes exist for all loadouts and dump / set to default if not.
var toRemove = new ValueList<string>();
foreach (var (roleName, loadouts) in _loadouts)
{
if (!prototypeManager.HasIndex<RoleLoadoutPrototype>(roleName))
{
toRemove.Add(roleName);
continue;
}
loadouts.EnsureValid(session, collection);
}
foreach (var value in toRemove)
{
_loadouts.Remove(value);
}
}
public ICharacterProfile Validated(IConfigurationManager configManager, IPrototypeManager prototypeManager)
public ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection)
{
var profile = new HumanoidCharacterProfile(this);
profile.EnsureValid(configManager, prototypeManager);
profile.EnsureValid(session, collection);
return profile;
}
@@ -551,16 +549,32 @@ namespace Content.Shared.Preferences
Age,
Sex,
Gender,
Appearance,
Clothing,
Backpack
Appearance
),
SpawnPriority,
PreferenceUnavailable,
_jobPriorities,
_antagPreferences,
_traitPreferences
_traitPreferences,
_loadouts
);
}
public void SetLoadout(RoleLoadout loadout)
{
_loadouts[loadout.Role.Id] = loadout;
}
public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
{
if (!_loadouts.TryGetValue(id, out var loadout))
{
loadout = new RoleLoadout(id);
loadout.SetDefault(protoManager, force: true);
}
loadout.SetDefault(protoManager);
return loadout;
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Humanoid;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences
@@ -15,11 +16,11 @@ namespace Content.Shared.Preferences
/// <summary>
/// Makes this profile valid so there's no bad data like negative ages.
/// </summary>
void EnsureValid(IConfigurationManager configManager, IPrototypeManager prototypeManager);
void EnsureValid(ICommonSession session, IDependencyCollection collection);
/// <summary>
/// Gets a copy of this profile that has <see cref="EnsureValid"/> applied, i.e. no invalid data.
/// </summary>
ICharacterProfile Validated(IConfigurationManager configManager, IPrototypeManager prototypeManager);
ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection);
}
}

View File

@@ -0,0 +1,29 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
/// <summary>
/// Uses a <see cref="LoadoutEffectGroupPrototype"/> prototype as a singular effect that can be re-used.
/// </summary>
public sealed partial class GroupLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
public ProtoId<LoadoutEffectGroupPrototype> Proto;
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
foreach (var effect in effectsProto.Effects)
{
if (!effect.Validate(loadout, session, collection, out reason))
return false;
}
reason = null;
return true;
}
}

View File

@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
/// <summary>
/// Checks for a job requirement to be met such as playtime.
/// </summary>
public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
public JobRequirement Requirement = default!;
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
var manager = collection.Resolve<ISharedPlaytimeManager>();
var playtimes = manager.GetPlayTimes(session);
return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason,
collection.Resolve<IEntityManager>(),
collection.Resolve<IPrototypeManager>());
}
}

View File

@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
[ImplicitDataDefinitionForInheritors]
public abstract partial class LoadoutEffect
{
/// <summary>
/// Tries to validate the effect.
/// </summary>
public abstract bool Validate(
RoleLoadout loadout,
ICommonSession session,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason);
public virtual void Apply(RoleLoadout loadout) {}
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts.Effects;
/// <summary>
/// Stores a group of loadout effects in a prototype for re-use.
/// </summary>
[Prototype]
public sealed class LoadoutEffectGroupPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
[DataField(required: true)]
public List<LoadoutEffect> Effects = new();
}

View File

@@ -0,0 +1,40 @@
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts.Effects;
public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
{
[DataField(required: true)]
public int Cost = 1;
public override bool Validate(
RoleLoadout loadout,
ICommonSession session,
IDependencyCollection collection,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
var protoManager = collection.Resolve<IPrototypeManager>();
if (!protoManager.TryIndex(loadout.Role, out var roleProto) || roleProto.Points == null)
{
return true;
}
if (loadout.Points <= Cost)
{
reason = FormattedMessage.FromUnformatted("loadout-group-points-insufficient");
return false;
}
return true;
}
public override void Apply(RoleLoadout loadout)
{
loadout.Points -= Cost;
}
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Specifies the selected prototype and custom data for a loadout.
/// </summary>
[Serializable, NetSerializable]
public sealed class Loadout
{
public ProtoId<LoadoutPrototype> Prototype;
}

View File

@@ -0,0 +1,34 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Corresponds to a set of loadouts for a particular slot.
/// </summary>
[Prototype("loadoutGroup")]
public sealed class LoadoutGroupPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
/// <summary>
/// User-friendly name for the group.
/// </summary>
[DataField(required: true)]
public LocId Name;
/// <summary>
/// Minimum number of loadouts that need to be specified for this category.
/// </summary>
[DataField]
public int MinLimit = 1;
/// <summary>
/// Maximum limit for the category.
/// </summary>
[DataField]
public int MaxLimit = 1;
[DataField(required: true)]
public List<ProtoId<LoadoutPrototype>> Loadouts = new();
}

View File

@@ -0,0 +1,25 @@
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Individual loadout item to be applied.
/// </summary>
[Prototype]
public sealed class LoadoutPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = string.Empty;
[DataField(required: true)]
public ProtoId<StartingGearPrototype> Equipment;
/// <summary>
/// Effects to be applied when the loadout is applied.
/// These can also return true or false for validation purposes.
/// </summary>
[DataField]
public List<LoadoutEffect> Effects = new();
}

View File

@@ -0,0 +1,248 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Random;
using Robust.Shared.Collections;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Contains all of the selected data for a role's loadout.
/// </summary>
[Serializable, NetSerializable]
public sealed class RoleLoadout
{
public readonly ProtoId<RoleLoadoutPrototype> Role;
public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
/*
* Loadout-specific data used for validation.
*/
public int? Points;
public RoleLoadout(ProtoId<RoleLoadoutPrototype> role)
{
Role = role;
}
/// <summary>
/// Ensures all prototypes exist and effects can be applied.
/// </summary>
public void EnsureValid(ICommonSession session, IDependencyCollection collection)
{
var groupRemove = new ValueList<string>();
var protoManager = collection.Resolve<IPrototypeManager>();
if (!protoManager.TryIndex(Role, out var roleProto))
{
SelectedLoadouts.Clear();
return;
}
// Reset points to recalculate.
Points = roleProto.Points;
foreach (var (group, groupLoadouts) in SelectedLoadouts)
{
// Dump if Group doesn't exist
if (!protoManager.TryIndex(group, out var groupProto))
{
groupRemove.Add(group);
continue;
}
var loadouts = groupLoadouts[..Math.Min(groupLoadouts.Count, groupProto.MaxLimit)];
// Validate first
for (var i = loadouts.Count - 1; i >= 0; i--)
{
var loadout = loadouts[i];
if (!protoManager.TryIndex(loadout.Prototype, out var loadoutProto))
{
loadouts.RemoveAt(i);
continue;
}
// Validate the loadout can be applied (e.g. points).
if (!IsValid(session, loadout.Prototype, collection, out _))
{
loadouts.RemoveAt(i);
continue;
}
Apply(loadoutProto);
}
// Apply defaults if required
// Technically it's possible for someone to game themselves into loadouts they shouldn't have
// If you put invalid ones first but that's your fault for not using sensible defaults
if (loadouts.Count < groupProto.MinLimit)
{
for (var i = 0; i < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); i++)
{
if (!protoManager.TryIndex(groupProto.Loadouts[i], out var loadoutProto))
continue;
var defaultLoadout = new Loadout()
{
Prototype = loadoutProto.ID,
};
if (loadouts.Contains(defaultLoadout))
continue;
// Still need to apply the effects even if validation is ignored.
loadouts.Add(defaultLoadout);
Apply(loadoutProto);
}
}
SelectedLoadouts[group] = loadouts;
}
foreach (var value in groupRemove)
{
SelectedLoadouts.Remove(value);
}
}
private void Apply(LoadoutPrototype loadoutProto)
{
foreach (var effect in loadoutProto.Effects)
{
effect.Apply(this);
}
}
/// <summary>
/// Resets the selected loadouts to default if no data is present.
/// </summary>
public void SetDefault(IPrototypeManager protoManager, bool force = false)
{
if (force)
SelectedLoadouts.Clear();
var roleProto = protoManager.Index(Role);
for (var i = roleProto.Groups.Count - 1; i >= 0; i--)
{
var group = roleProto.Groups[i];
if (!protoManager.TryIndex(group, out var groupProto))
continue;
if (SelectedLoadouts.ContainsKey(group))
continue;
SelectedLoadouts[group] = new List<Loadout>();
if (groupProto.MinLimit > 0)
{
// Apply any loadouts we can.
for (var j = 0; j < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); j++)
{
AddLoadout(group, groupProto.Loadouts[j], protoManager);
}
}
}
}
/// <summary>
/// Returns whether a loadout is valid or not.
/// </summary>
public bool IsValid(ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
var protoManager = collection.Resolve<IPrototypeManager>();
if (!protoManager.TryIndex(loadout, out var loadoutProto))
{
// Uhh
reason = FormattedMessage.FromMarkup("");
return false;
}
if (!protoManager.TryIndex(Role, out var roleProto))
{
reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
return false;
}
var valid = true;
foreach (var effect in loadoutProto.Effects)
{
valid = valid && effect.Validate(this, session, collection, out reason);
}
return valid;
}
/// <summary>
/// Applies the specified loadout to this group.
/// </summary>
public bool AddLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
{
var groupLoadouts = SelectedLoadouts[selectedGroup];
// Need to unselect existing ones if we're at or above limit
var limit = Math.Max(0, groupLoadouts.Count + 1 - protoManager.Index(selectedGroup).MaxLimit);
for (var i = 0; i < groupLoadouts.Count; i++)
{
var loadout = groupLoadouts[i];
if (loadout.Prototype != selectedLoadout)
{
// Remove any other loadouts that might push it above the limit.
if (limit > 0)
{
limit--;
groupLoadouts.RemoveAt(i);
i--;
}
continue;
}
DebugTools.Assert(false);
return false;
}
groupLoadouts.Add(new Loadout()
{
Prototype = selectedLoadout,
});
return true;
}
/// <summary>
/// Removed the specified loadout from this group.
/// </summary>
public bool RemoveLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
{
// Although this may bring us below minimum we'll let EnsureValid handle it.
var groupLoadouts = SelectedLoadouts[selectedGroup];
for (var i = 0; i < groupLoadouts.Count; i++)
{
var loadout = groupLoadouts[i];
if (loadout.Prototype != selectedLoadout)
continue;
groupLoadouts.RemoveAt(i);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Preferences.Loadouts;
/// <summary>
/// Corresponds to a Job / Antag prototype and specifies loadouts
/// </summary>
[Prototype]
public sealed class RoleLoadoutPrototype : IPrototype
{
/*
* Separate to JobPrototype / AntagPrototype as they are turning into messy god classes.
*/
[IdDataField]
public string ID { get; } = string.Empty;
/// <summary>
/// Groups that comprise this role loadout.
/// </summary>
[DataField(required: true)]
public List<ProtoId<LoadoutGroupPrototype>> Groups = new();
/// <summary>
/// How many points are allotted for this role loadout prototype.
/// </summary>
[DataField]
public int? Points;
}

View File

@@ -96,7 +96,7 @@ namespace Content.Shared.Roles
/// </summary>
public static bool TryRequirementMet(
JobRequirement requirement,
Dictionary<string, TimeSpan> playTimes,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager prototypes)

View File

@@ -9,37 +9,21 @@ namespace Content.Shared.Roles
[DataField]
public Dictionary<string, EntProtoId> Equipment = new();
/// <summary>
/// if empty, there is no skirt override - instead the uniform provided in equipment is added.
/// </summary>
[DataField]
public EntProtoId? InnerClothingSkirt;
[DataField]
public EntProtoId? Satchel;
[DataField]
public EntProtoId? Duffelbag;
[DataField]
public List<EntProtoId> Inhand = new(0);
/// <summary>
/// Inserts entities into the specified slot's storage (if it does have storage).
/// </summary>
[DataField]
public Dictionary<string, List<EntProtoId>> Storage = new();
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = string.Empty;
public string GetGear(string slot, HumanoidCharacterProfile? profile)
public string GetGear(string slot)
{
if (profile != null)
{
if (slot == "jumpsuit" && profile.Clothing == ClothingPreference.Jumpskirt && !string.IsNullOrEmpty(InnerClothingSkirt))
return InnerClothingSkirt;
if (slot == "back" && profile.Backpack == BackpackPreference.Satchel && !string.IsNullOrEmpty(Satchel))
return Satchel;
if (slot == "back" && profile.Backpack == BackpackPreference.Duffelbag && !string.IsNullOrEmpty(Duffelbag))
return Duffelbag;
}
return Equipment.TryGetValue(slot, out var equipment) ? equipment : string.Empty;
}
}

View File

@@ -3,6 +3,9 @@ using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Storage;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Collections;
namespace Content.Shared.Station;
@@ -10,40 +13,69 @@ public abstract class SharedStationSpawningSystem : EntitySystem
{
[Dependency] protected readonly InventorySystem InventorySystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
/// <summary>
/// Equips starting gear onto the given entity.
/// </summary>
/// <param name="entity">Entity to load out.</param>
/// <param name="startingGear">Starting gear to use.</param>
/// <param name="profile">Character profile to use, if any.</param>
public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile)
public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear)
{
if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
{
foreach (var slot in slotDefinitions)
{
var equipmentStr = startingGear.GetGear(slot.Name, profile);
var equipmentStr = startingGear.GetGear(slot.Name);
if (!string.IsNullOrEmpty(equipmentStr))
{
var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true, force:true);
InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, silent: true, force:true);
}
}
}
if (!TryComp(entity, out HandsComponent? handsComponent))
return;
var inhand = startingGear.Inhand;
var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
foreach (var prototype in inhand)
if (TryComp(entity, out HandsComponent? handsComponent))
{
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
var inhand = startingGear.Inhand;
var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
foreach (var prototype in inhand)
{
_handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
{
_handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
}
}
}
if (startingGear.Storage.Count > 0)
{
var coords = _xformSystem.GetMapCoordinates(entity);
var ents = new ValueList<EntityUid>();
TryComp(entity, out InventoryComponent? inventoryComp);
foreach (var (slot, entProtos) in startingGear.Storage)
{
if (entProtos.Count == 0)
continue;
foreach (var ent in entProtos)
{
ents.Add(Spawn(ent, coords));
}
if (inventoryComp != null &&
InventorySystem.TryGetSlotEntity(entity, slot, out var slotEnt, inventoryComponent: inventoryComp) &&
TryComp(slotEnt, out StorageComponent? storage))
{
foreach (var ent in ents)
{
_storage.Insert(slotEnt.Value, ent, out _, storageComp: storage, playSound: false);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
loadout-window = Loadout
loadout-none = None

View File

@@ -0,0 +1,159 @@
# Miscellaneous
loadout-group-trinkets = Trinkets
# Command
loadout-group-captain-head = Captain head
loadout-group-captain-jumpsuit = Captain jumpsuit
loadout-group-captain-neck = Captain neck
loadout-group-captain-backpack = Captain backpack
loadout-group-hop-head = Head of Personnel head
loadout-group-hop-jumpsuit = Head of Personnel jumpsuit
loadout-group-hop-neck = Head of Personnel neck
loadout-group-hop-backpack = Head of Personnel backpack
# Civilian
loadout-group-passenger-jumpsuit = Passenger jumpsuit
loadout-group-passenger-mask = Passenger mask
loadout-group-passenger-gloves = Passenger gloves
loadout-group-passenger-backpack = Passenger backpack
loadout-group-bartender-head = Bartender head
loadout-group-bartender-jumpsuit = Bartender jumpsuit
loadout-group-bartender-outerclothing = Bartender outer clothing
loadout-group-chef-head = Chef head
loadout-group-chef-mask = Chef mask
loadout-group-chef-jumpsuit = Chef jumpsuit
loadout-group-chef-outerclothing = Chef outer clothing
loadout-group-librarian-jumpsuit = Librarian jumpsuit
loadout-group-lawyer-jumpsuit = Lawyer jumpsuit
loadout-group-lawyer-neck = Lawyer neck
loadout-group-chaplain-head = Chaplain head
loadout-group-chaplain-mask = Chaplain mask
loadout-group-chaplain-jumpsuit = Chaplain jumpsuit
loadout-group-chaplain-backpack = Chaplain backpack
loadout-group-chaplain-outerclothing = Chaplain outer clothing
loadout-group-chaplain-neck = Chaplain neck
loadout-group-janitor-head = Janitor head
loadout-group-janitor-jumpsuit = Janitor jumpsuit
loadout-group-botanist-head = Botanist head
loadout-group-botanist-jumpsuit = Botanist jumpsuit
loadout-group-botanist-backpack = Botanist backpack
loadout-group-botanist-outerclothing = Botanist outer clothing
loadout-group-clown-head = Clown head
loadout-group-clown-jumpsuit = Clown jumpsuit
loadout-group-clown-backpack = Clown backpack
loadout-group-clown-shoes = Clown shoes
loadout-group-mime-head = Mime head
loadout-group-mime-mask = Mime mask
loadout-group-mime-jumpsuit = Mime jumpsuit
loadout-group-mime-backpack = Mime backpack
loadout-group-musician-backpack = Musician backpack
# Cargo
loadout-group-quartermaster-head = Quartermaster head
loadout-group-quartermaster-jumpsuit = Quartermaster jumpsuit
loadout-group-quartermaster-backpack = Quartermaster backpack
loadout-group-quartermaster-neck = Quartermaster neck
loadout-group-cargo-technician-head = Technician head
loadout-group-cargo-technician-jumpsuit = Technician jumpsuit
loadout-group-cargo-technician-backpack = Technician backpack
loadout-group-salvage-specialist-backpack = Salvage Specialist backpack
# Engineering
loadout-group-chief-engineer-head = Chief Engineer head
loadout-group-chief-engineer-jumpsuit = Chief Engineer jumpsuit
loadout-group-chief-engineer-backpack = Chief Engineer backpack
loadout-group-chief-engineer-neck = Chief Engineer neck
loadout-group-technical-assistant-jumpsuit = Technical Assistant jumpsuit
loadout-group-station-engineer-head = Station Engineer head
loadout-group-station-engineer-jumpsuit = Station Engineer jumpsuit
loadout-group-station-engineer-backpack = Station Engineer backpack
loadout-group-station-engineer-outerclothing = Station Engineer outer clothing
loadout-group-station-engineer-id = Station Engineer ID
loadout-group-atmospheric-technician-jumpsuit = Atmospheric Technician jumpsuit
loadout-group-atmospheric-technician-backpack = Atmospheric Technician backpack
# Science
loadout-group-research-director-head = Research Director head
loadout-group-research-director-neck = Research Director neck
loadout-group-research-director-jumpsuit = Research Director jumpsuit
loadout-group-research-director-backpack = Research Director backpack
loadout-group-research-director-outerclothing = Research Director outer clothing
loadout-group-scientist-head = Scientist head
loadout-group-scientist-neck = Scientist neck
loadout-group-scientist-jumpsuit = Scientist jumpsuit
loadout-group-scientist-backpack = Scientist backpack
loadout-group-scientist-outerclothing = Scientist outer clothing
loadout-group-scientist-id = Scientist ID
loadout-group-research-assistant-jumpsuit = Research Assistant jumpsuit
# Security
loadout-group-head-of-security-head = Head of Security head
loadout-group-head-of-security-jumpsuit = Head of Security jumpsuit
loadout-group-head-of-security-neck = Head of Security neck
loadout-group-head-of-security-outerclothing = Head of Security outer clothing
loadout-group-warden-head = Warden head
loadout-group-warden-jumpsuit = Warden jumpsuit
loadout-group-warden-outerclothing = Warden outer clothing
loadout-group-security-head = Security head
loadout-group-security-jumpsuit = Security jumpsuit
loadout-group-security-backpack = Security backpack
loadout-group-security-id = Security ID
loadout-group-detective-head = Detective head
loadout-group-detective-neck = Detective neck
loadout-group-detective-jumpsuit = Detective jumpsuit
loadout-group-detective-backpack = Detective backpack
loadout-group-detective-outerclothing = Detective outer clothing
loadout-group-security-cadet-jumpsuit = Security cadet jumpsuit
# Medical
loadout-group-chief-medical-officer-head = Chief Medical Officer head
loadout-group-chief-medical-officer-jumpsuit = Chief Medical Officer jumpsuit
loadout-group-chief-medical-officer-outerclothing = Chief Medical Officer outer clothing
loadout-group-chief-medical-officer-backpack = Chief Medical Officer backpack
loadout-group-chief-medical-officer-neck = Chief Medical Officer neck
loadout-group-medical-doctor-head = Medical Doctor head
loadout-group-medical-doctor-jumpsuit = Medical Doctor jumpsuit
loadout-group-medical-doctor-outerclothing = Medical Doctor outer clothing
loadout-group-medical-doctor-backpack = Medical Doctor backpack
loadout-group-medical-doctor-id = Medical Doctor ID
loadout-group-medical-intern-jumpsuit = Medical intern jumpsuit
loadout-group-chemist-jumpsuit = Chemist jumpsuit
loadout-group-chemist-outerclothing = Chemist outer clothing
loadout-group-chemist-backpack = Chemist backpack
loadout-group-paramedic-head = Paramedic head
loadout-group-paramedic-jumpsuit = Paramedic jumpsuit
loadout-group-paramedic-outerclothing = Paramedic outer clothing
loadout-group-paramedic-backpack = Paramedic backpack
# Wildcards
loadout-group-reporter-jumpsuit = Reporter jumpsuit
loadout-group-boxer-jumpsuit = Boxer jumpsuit
loadout-group-boxer-gloves = Boxer gloves

View File

@@ -0,0 +1,7 @@
# Restrictions
loadout-restrictions = Restrictions
loadouts-min-limit = Min count: {$count}
loadouts-max-limit = Max count: {$count}
loadouts-points-limit = Points: {$count} / {$max}
loadouts-points-restriction = Insufficient points

View File

@@ -19,8 +19,6 @@ humanoid-profile-editor-pronouns-neuter-text = It / It
humanoid-profile-editor-import-button = Import
humanoid-profile-editor-export-button = Export
humanoid-profile-editor-save-button = Save
humanoid-profile-editor-clothing-label = Clothing:
humanoid-profile-editor-backpack-label = Backpack:
humanoid-profile-editor-spawn-priority-label = Spawn priority:
humanoid-profile-editor-eyes-label = Eye color:
humanoid-profile-editor-jobs-tab = Jobs

View File

@@ -104,6 +104,17 @@
- id: Flash
#- id: TelescopicBaton
- type: entity
noSpawn: true
parent: ClothingBackpackIan
id: ClothingBackpackHOPIanFilled
components:
- type: StorageFill
contents:
- id: BoxSurvival
- id: Flash
#- id: TelescopicBaton
- type: entity
noSpawn: true
parent: ClothingBackpackMedical

View File

@@ -0,0 +1,56 @@
# Head
- type: loadout
id: CargoTechnicianHead
equipment: CargoTechnicianHead
- type: startingGear
id: CargoTechnicianHead
equipment:
head: ClothingHeadHatCargosoft
# Jumpsuit
- type: loadout
id: CargoTechnicianJumpsuit
equipment: CargoTechnicianJumpsuit
- type: startingGear
id: CargoTechnicianJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitCargo
- type: loadout
id: CargoTechnicianJumpskirt
equipment: CargoTechnicianJumpskirt
- type: startingGear
id: CargoTechnicianJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtCargo
# Back
- type: loadout
id: CargoTechnicianBackpack
equipment: CargoTechnicianBackpack
- type: startingGear
id: CargoTechnicianBackpack
equipment:
back: ClothingBackpackCargoFilled
- type: loadout
id: CargoTechnicianSatchel
equipment: CargoTechnicianSatchel
- type: startingGear
id: CargoTechnicianSatchel
equipment:
back: ClothingBackpackSatchelCargoFilled
- type: loadout
id: CargoTechnicianDuffel
equipment: CargoTechnicianDuffel
- type: startingGear
id: CargoTechnicianDuffel
equipment:
back: ClothingBackpackDuffelCargoFilled

View File

@@ -0,0 +1,111 @@
# Jumpsuit
- type: loadout
id: QuartermasterJumpsuit
equipment: QuartermasterJumpsuit
- type: startingGear
id: QuartermasterJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitQM
- type: loadout
id: QuartermasterJumpskirt
equipment: QuartermasterJumpskirt
- type: startingGear
id: QuartermasterJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtQM
- type: loadout
id: QuartermasterTurtleneck
equipment: QuartermasterTurtleneck
- type: startingGear
id: QuartermasterTurtleneck
equipment:
jumpsuit: ClothingUniformJumpsuitQMTurtleneck
- type: loadout
id: QuartermasterTurtleneckSkirt
equipment: QuartermasterTurtleneckSkirt
- type: startingGear
id: QuartermasterTurtleneckSkirt
equipment:
jumpsuit: ClothingUniformJumpskirtQMTurtleneck
- type: loadout
id: QuartermasterFormalSuit
equipment: QuartermasterFormalSuit
- type: startingGear
id: QuartermasterFormalSuit
equipment:
jumpsuit: ClothingUniformJumpsuitQMFormal
# Head
- type: loadout
id: QuartermasterHead
equipment: QuartermasterHead
- type: startingGear
id: QuartermasterHead
equipment:
head: ClothingHeadHatQMsoft
- type: loadout
id: QuartermasterBeret
equipment: QuartermasterBeret
- type: startingGear
id: QuartermasterBeret
equipment:
head: ClothingHeadHatBeretQM
# Neck
- type: loadout
id: QuartermasterCloak
equipment: QuartermasterCloak
- type: startingGear
id: QuartermasterCloak
equipment:
neck: ClothingNeckCloakQm
- type: loadout
id: QuartermasterMantle
equipment: QuartermasterMantle
- type: startingGear
id: QuartermasterMantle
equipment:
neck: ClothingNeckMantleQM
# Back
- type: loadout
id: QuartermasterBackpack
equipment: QuartermasterBackpack
- type: startingGear
id: QuartermasterBackpack
equipment:
back: ClothingBackpackQuartermasterFilled
- type: loadout
id: QuartermasterSatchel
equipment: QuartermasterSatchel
- type: startingGear
id: QuartermasterSatchel
equipment:
back: ClothingBackpackSatchelQuartermasterFilled
- type: loadout
id: QuartermasterDuffel
equipment: QuartermasterDuffel
- type: startingGear
id: QuartermasterDuffel
equipment:
back: ClothingBackpackDuffelQuartermasterFilled

View File

@@ -0,0 +1,27 @@
# Back
- type: loadout
id: SalvageSpecialistBackpack
equipment: SalvageSpecialistBackpack
- type: startingGear
id: SalvageSpecialistBackpack
equipment:
back: ClothingBackpackSalvageFilled
- type: loadout
id: SalvageSpecialistSatchel
equipment: SalvageSpecialistSatchel
- type: startingGear
id: SalvageSpecialistSatchel
equipment:
back: ClothingBackpackSatchelSalvageFilled
- type: loadout
id: SalvageSpecialistDuffel
equipment: SalvageSpecialistDuffel
- type: startingGear
id: SalvageSpecialistDuffel
equipment:
back: ClothingBackpackDuffelSalvageFilled

View File

@@ -0,0 +1,65 @@
# Head
- type: loadout
id: BartenderHead
equipment: BartenderHead
- type: startingGear
id: BartenderHead
equipment:
head: ClothingHeadHatTophat
- type: loadout
id: BartenderBowler
equipment: BartenderBowler
- type: startingGear
id: BartenderBowler
equipment:
head: ClothingHeadHatBowlerHat
# Jumpsuit
- type: loadout
id: BartenderJumpsuit
equipment: BartenderJumpsuit
- type: startingGear
id: BartenderJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitBartender
- type: loadout
id: BartenderJumpskirt
equipment: BartenderJumpskirt
- type: startingGear
id: BartenderJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtBartender
- type: loadout
id: BartenderJumpsuitPurple
equipment: BartenderJumpsuitPurple
- type: startingGear
id: BartenderJumpsuitPurple
equipment:
jumpsuit: ClothingUniformJumpsuitBartenderPurple
# Outer clothing
- type: loadout
id: BartenderApron
equipment: BartenderApron
- type: startingGear
id: BartenderApron
equipment:
outerClothing: ClothingOuterApronBar
- type: loadout
id: BartenderVest
equipment: BartenderVest
- type: startingGear
id: BartenderVest
equipment:
outerClothing: ClothingOuterVest

View File

@@ -0,0 +1,84 @@
# Head
- type: loadout
id: BotanistHead
equipment: BotanistHead
- type: startingGear
id: BotanistHead
equipment:
head: ClothingHeadHatTrucker
- type: loadout
id: BotanistBandana
equipment: BotanistBandana
- type: startingGear
id: BotanistBandana
equipment:
head: ClothingHeadBandBotany
# Jumpsuit
- type: loadout
id: BotanistJumpsuit
equipment: BotanistJumpsuit
- type: startingGear
id: BotanistJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitHydroponics
- type: loadout
id: BotanistJumpskirt
equipment: BotanistJumpskirt
- type: startingGear
id: BotanistJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtHydroponics
- type: loadout
id: BotanistOveralls
equipment: BotanistOveralls
- type: startingGear
id: BotanistOveralls
equipment:
jumpsuit: ClothingUniformOveralls
# Back
- type: loadout
id: BotanistBackpack
equipment: BotanistBackpack
- type: startingGear
id: BotanistBackpack
equipment:
back: ClothingBackpackHydroponicsFilled
- type: loadout
id: BotanistSatchel
equipment: BotanistSatchel
- type: startingGear
id: BotanistSatchel
equipment:
back: ClothingBackpackSatchelHydroponicsFilled
- type: loadout
id: BotanistDuffel
equipment: BotanistDuffel
- type: startingGear
id: BotanistDuffel
equipment:
back: ClothingBackpackDuffelHydroponicsFilled
# Outer clothing
- type: loadout
id: BotanistApron
equipment: BotanistApron
- type: startingGear
id: BotanistApron
equipment:
outerClothing: ClothingOuterApronBotanist

View File

@@ -0,0 +1,158 @@
# Head
- type: loadout
id: ChaplainHead
equipment: ChaplainHead
- type: startingGear
id: ChaplainHead
equipment:
head: ClothingHeadHatFez
- type: loadout
id: ChaplainPlagueHat
equipment: ChaplainPlagueHat
- type: startingGear
id: ChaplainPlagueHat
equipment:
head: ClothingHeadHatPlaguedoctor
- type: loadout
id: ChaplainWitchHat
equipment: ChaplainWitchHat
- type: startingGear
id: ChaplainWitchHat
equipment:
head: ClothingHeadHatWitch
- type: loadout
id: ChaplainWitchHatAlt
equipment: ChaplainWitchHatAlt
- type: startingGear
id: ChaplainWitchHatAlt
equipment:
head: ClothingHeadHatWitch1
# Mask
- type: loadout
id: ChaplainMask
equipment: ChaplainMask
- type: startingGear
id: ChaplainMask
equipment:
mask: ClothingMaskPlague
# Jumpsuit
- type: loadout
id: ChaplainJumpsuit
equipment: ChaplainJumpsuit
- type: startingGear
id: ChaplainJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitChaplain
- type: loadout
id: ChaplainJumpskirt
equipment: ChaplainJumpskirt
- type: startingGear
id: ChaplainJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtChaplain
- type: loadout
id: ChaplainRobesDark
equipment: ChaplainRobesDark
- type: startingGear
id: ChaplainRobesDark
equipment:
jumpsuit: ClothingUniformJumpsuitMonasticRobeDark
- type: loadout
id: ChaplainRobesLight
equipment: ChaplainRobesLight
- type: startingGear
id: ChaplainRobesLight
equipment:
jumpsuit: ClothingUniformJumpsuitMonasticRobeLight
# Back
- type: loadout
id: ChaplainBackpack
equipment: ChaplainBackpack
- type: startingGear
id: ChaplainBackpack
equipment:
back: ClothingBackpackChaplainFilled
- type: loadout
id: ChaplainSatchel
equipment: ChaplainSatchel
- type: startingGear
id: ChaplainSatchel
equipment:
back: ClothingBackpackSatchelChaplainFilled
- type: loadout
id: ChaplainDuffel
equipment: ChaplainDuffel
- type: startingGear
id: ChaplainDuffel
equipment:
back: ClothingBackpackDuffelChaplainFilled
# Neck
- type: loadout
id: ChaplainNeck
equipment: ChaplainNeck
- type: startingGear
id: ChaplainNeck
equipment:
neck: ClothingNeckStoleChaplain
# Outer clothing
- type: loadout
id: ChaplainPlagueSuit
equipment: ChaplainPlagueSuit
- type: startingGear
id: ChaplainPlagueSuit
equipment:
outerClothing: ClothingOuterPlagueSuit
- type: loadout
id: ChaplainNunRobe
equipment: ChaplainNunRobe
- type: startingGear
id: ChaplainNunRobe
equipment:
outerClothing: ClothingOuterNunRobe
- type: loadout
id: ChaplainBlackHoodie
equipment: ChaplainBlackHoodie
- type: startingGear
id: ChaplainBlackHoodie
equipment:
outerClothing: ClothingOuterHoodieBlack
- type: loadout
id: ChaplainHoodie
equipment: ChaplainHoodie
- type: startingGear
id: ChaplainHoodie
equipment:
outerClothing: ClothingOuterHoodieChaplain

View File

@@ -0,0 +1,57 @@
# Head
- type: loadout
id: ChefHead
equipment: ChefHead
- type: startingGear
id: ChefHead
equipment:
head: ClothingHeadHatChef
# Mask
- type: loadout
id: ChefMask
equipment: ChefMask
- type: startingGear
id: ChefMask
equipment:
mask: ClothingMaskItalianMoustache
# Jumpsuit
- type: loadout
id: ChefJumpsuit
equipment: ChefJumpsuit
- type: startingGear
id: ChefJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitChef
- type: loadout
id: ChefJumpskirt
equipment: ChefJumpskirt
- type: startingGear
id: ChefJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtChef
# Outer clothing
- type: loadout
id: ChefApron
equipment: ChefApron
- type: startingGear
id: ChefApron
equipment:
outerClothing: ClothingOuterApronChef
- type: loadout
id: ChefJacket
equipment: ChefJacket
- type: startingGear
id: ChefJacket
equipment:
outerClothing: ClothingOuterJacketChef

View File

@@ -0,0 +1,75 @@
# Head
- type: loadout
id: JesterHat
equipment: JesterHat
- type: startingGear
id: JesterHat
equipment:
head: ClothingHeadHatJesterAlt
# Jumpsuit
- type: loadout
id: ClownSuit
equipment: ClownSuit
- type: startingGear
id: ClownSuit
equipment:
jumpsuit: ClothingUniformJumpsuitClown
- type: loadout
id: JesterSuit
equipment: JesterSuit
- type: startingGear
id: JesterSuit
equipment:
jumpsuit: ClothingUniformJumpsuitJesterAlt
# Back
- type: loadout
id: ClownBackpack
equipment: ClownBackpack
- type: startingGear
id: ClownBackpack
equipment:
back: ClothingBackpackClownFilled
- type: loadout
id: ClownSatchel
equipment: ClownSatchel
- type: startingGear
id: ClownSatchel
equipment:
back: ClothingBackpackSatchelClownFilled
- type: loadout
id: ClownDuffel
equipment: ClownDuffel
- type: startingGear
id: ClownDuffel
equipment:
back: ClothingBackpackDuffelClownFilled
# Shoes
- type: loadout
id: ClownShoes
equipment: ClownShoes
- type: startingGear
id: ClownShoes
equipment:
shoes: ClothingShoesClown
- type: loadout
id: JesterShoes
equipment: JesterShoes
- type: startingGear
id: JesterShoes
equipment:
shoes: ClothingShoesJester

View File

@@ -0,0 +1,28 @@
# Head
- type: loadout
id: JanitorHead
equipment: JanitorHead
- type: startingGear
id: JanitorHead
equipment:
head: ClothingHeadHatPurplesoft
# Jumpsuit
- type: loadout
id: JanitorJumpsuit
equipment: JanitorJumpsuit
- type: startingGear
id: JanitorJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitJanitor
- type: loadout
id: JanitorJumpskirt
equipment: JanitorJumpskirt
- type: startingGear
id: JanitorJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtJanitor

View File

@@ -0,0 +1,100 @@
# Jumpsuit
- type: loadout
id: LawyerJumpsuit
equipment: LawyerJumpsuit
- type: startingGear
id: LawyerJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitLawyerBlack
- type: loadout
id: LawyerJumpskirt
equipment: LawyerJumpskirt
- type: startingGear
id: LawyerJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtLawyerBlack
- type: loadout
id: LawyerJumpsuitBlue
equipment: LawyerJumpsuitBlue
- type: startingGear
id: LawyerJumpsuitBlue
equipment:
jumpsuit: ClothingUniformJumpsuitLawyerBlue
- type: loadout
id: LawyerJumpskirtBlue
equipment: LawyerJumpskirtBlue
- type: startingGear
id: LawyerJumpskirtBlue
equipment:
jumpsuit: ClothingUniformJumpskirtLawyerBlue
- type: loadout
id: LawyerJumpsuitPurple
equipment: LawyerJumpsuitPurple
- type: startingGear
id: LawyerJumpsuitPurple
equipment:
jumpsuit: ClothingUniformJumpsuitLawyerPurple
- type: loadout
id: LawyerJumpskirtPurple
equipment: LawyerJumpskirtPurple
- type: startingGear
id: LawyerJumpskirtPurple
equipment:
jumpsuit: ClothingUniformJumpskirtLawyerPurple
- type: loadout
id: LawyerJumpsuitRed
equipment: LawyerJumpsuitRed
- type: startingGear
id: LawyerJumpsuitRed
equipment:
jumpsuit: ClothingUniformJumpsuitLawyerRed
- type: loadout
id: LawyerJumpskirtRed
equipment: LawyerJumpskirtRed
- type: startingGear
id: LawyerJumpskirtRed
equipment:
jumpsuit: ClothingUniformJumpskirtLawyerRed
- type: loadout
id: LawyerJumpsuitGood
equipment: LawyerJumpsuitGood
- type: startingGear
id: LawyerJumpsuitGood
equipment:
jumpsuit: ClothingUniformJumpsuitLawyerGood
- type: loadout
id: LawyerJumpskirtGood
equipment: LawyerJumpskirtGood
- type: startingGear
id: LawyerJumpskirtGood
equipment:
jumpsuit: ClothingUniformJumpskirtLawyerGood
# Neck
- type: loadout
id: LawyerNeck
equipment: LawyerNeck
- type: startingGear
id: LawyerNeck
equipment:
neck: ClothingNeckLawyerbadge

View File

@@ -0,0 +1,36 @@
# Jumpsuit
- type: loadout
id: LibrarianJumpsuit
equipment: LibrarianJumpsuit
- type: startingGear
id: LibrarianJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitLibrarian
- type: loadout
id: LibrarianJumpskirt
equipment: LibrarianJumpskirt
- type: startingGear
id: LibrarianJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtLibrarian
- type: loadout
id: CuratorJumpsuit
equipment: CuratorJumpsuit
- type: startingGear
id: CuratorJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitCurator
- type: loadout
id: CuratorJumpskirt
equipment: CuratorJumpskirt
- type: startingGear
id: CuratorJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtCurator

View File

@@ -0,0 +1,102 @@
# Head
- type: loadout
id: MimeHead
equipment: MimeHead
- type: startingGear
id: MimeHead
equipment:
head: ClothingHeadHatBeret
- type: loadout
id: MimeFrenchBeret
equipment: MimeFrenchBeret
- type: startingGear
id: MimeFrenchBeret
equipment:
head: ClothingHeadHatBeretFrench
- type: loadout
id: MimeCap
equipment: MimeCap
- type: startingGear
id: MimeCap
equipment:
head: ClothingHeadHatMimesoft
# Mask
- type: loadout
id: MimeMask
equipment: MimeMask
- type: startingGear
id: MimeMask
equipment:
mask: ClothingMaskMime
- type: loadout
id: MimeMaskSad
equipment: MimeMaskSad
- type: startingGear
id: MimeMaskSad
equipment:
mask: ClothingMaskSadMime
- type: loadout
id: MimeMaskScared
equipment: MimeMaskScared
- type: startingGear
id: MimeMaskScared
equipment:
mask: ClothingMaskScaredMime
# Jumpsuit
- type: loadout
id: MimeJumpsuit
equipment: MimeJumpsuit
- type: startingGear
id: MimeJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitMime
- type: loadout
id: MimeJumpskirt
equipment: MimeJumpskirt
- type: startingGear
id: MimeJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtMime
# Back
- type: loadout
id: MimeBackpack
equipment: MimeBackpack
- type: startingGear
id: MimeBackpack
equipment:
back: ClothingBackpackMimeFilled
- type: loadout
id: MimeSatchel
equipment: MimeSatchel
- type: startingGear
id: MimeSatchel
equipment:
back: ClothingBackpackSatchelMimeFilled
- type: loadout
id: MimeDuffel
equipment: MimeDuffel
- type: startingGear
id: MimeDuffel
equipment:
back: ClothingBackpackDuffelMimeFilled

View File

@@ -0,0 +1,27 @@
# Back
- type: loadout
id: MusicianBackpack
equipment: MusicianBackpack
- type: startingGear
id: MusicianBackpack
equipment:
back: ClothingBackpackMusicianFilled
- type: loadout
id: MusicianSatchel
equipment: MusicianSatchel
- type: startingGear
id: MusicianSatchel
equipment:
back: ClothingBackpackSatchelMusicianFilled
- type: loadout
id: MusicianDuffel
equipment: MusicianDuffel
- type: startingGear
id: MusicianDuffel
equipment:
back: ClothingBackpackDuffelMusicianFilled

View File

@@ -0,0 +1,82 @@
# Greytide Time
- type: loadoutEffectGroup
id: GreyTider
effects:
- !type:JobRequirementLoadoutEffect
requirement:
!type:RoleTimeRequirement
role: JobPassenger
time: 36000 #10 hrs, silly reward for people who play passenger a lot
# Face
- type: loadout
id: PassengerFace
equipment: GasMask
effects:
- !type:GroupLoadoutEffect
proto: GreyTider
- type: startingGear
id: GasMask
equipment:
mask: ClothingMaskGas
# Jumpsuit
- type: loadout
id: GreyJumpsuit
equipment: GreyJumpsuit
- type: startingGear
id: GreyJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitColorGrey
- type: loadout
id: GreyJumpskirt
equipment: GreyJumpskirt
- type: startingGear
id: GreyJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtColorGrey
# Back
- type: loadout
id: CommonBackpack
equipment: CommonBackpack
- type: startingGear
id: CommonBackpack
equipment:
back: ClothingBackpackFilled
- type: loadout
id: CommonSatchel
equipment: CommonSatchel
- type: startingGear
id: CommonSatchel
equipment:
back: ClothingBackpackSatchelFilled
- type: loadout
id: CommonDuffel
equipment: CommonDuffel
- type: startingGear
id: CommonDuffel
equipment:
back: ClothingBackpackDuffelFilled
# Gloves
- type: loadout
id: PassengerGloves
equipment: FingerlessInsulatedGloves
effects:
- !type:GroupLoadoutEffect
proto: GreyTider
- type: startingGear
id: FingerlessInsulatedGloves
equipment:
gloves: ClothingHandsGlovesFingerlessInsulated

View File

@@ -0,0 +1,111 @@
# Jumpsuit
- type: loadout
id: CaptainJumpsuit
equipment: CaptainJumpsuit
- type: startingGear
id: CaptainJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitCaptain
- type: loadout
id: CaptainJumpskirt
equipment: CaptainJumpskirt
- type: startingGear
id: CaptainJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtCaptain
- type: loadout
id: CaptainFormalSuit
equipment: CaptainFormalSuit
- type: startingGear
id: CaptainFormalSuit
equipment:
jumpsuit: ClothingUniformJumpsuitCapFormal
- type: loadout
id: CaptainFormalSkirt
equipment: CaptainFormalSkirt
- type: startingGear
id: CaptainFormalSkirt
equipment:
jumpsuit: ClothingUniformJumpskirtCapFormalDress
# Head
- type: loadout
id: CaptainHead
equipment: CaptainHead
- type: startingGear
id: CaptainHead
equipment:
head: ClothingHeadHatCaptain
- type: loadout
id: CaptainCap
equipment: CaptainCap
- type: startingGear
id: CaptainCap
equipment:
head: ClothingHeadHatCapcap
# Neck
- type: loadout
id: CaptainCloak
equipment: CaptainCloak
- type: startingGear
id: CaptainCloak
equipment:
neck: ClothingNeckCloakCap
- type: loadout
id: CaptainCloakFormal
equipment: CaptainCloakFormal
- type: startingGear
id: CaptainCloakFormal
equipment:
neck: ClothingNeckCloakCapFormal
- type: loadout
id: CaptainMantle
equipment: CaptainMantle
- type: startingGear
id: CaptainMantle
equipment:
neck: ClothingNeckMantleCap
# Back
- type: loadout
id: CaptainBackpack
equipment: CaptainBackpack
- type: startingGear
id: CaptainBackpack
equipment:
back: ClothingBackpackCaptainFilled
- type: loadout
id: CaptainSatchel
equipment: CaptainSatchel
- type: startingGear
id: CaptainSatchel
equipment:
back: ClothingBackpackSatchelCaptainFilled
- type: loadout
id: CaptainDuffel
equipment: CaptainDuffel
- type: startingGear
id: CaptainDuffel
equipment:
back: ClothingBackpackDuffelCaptainFilled

View File

@@ -0,0 +1,97 @@
# Professional HoP Time
- type: loadoutEffectGroup
id: ProfessionalHoP
effects:
- !type:JobRequirementLoadoutEffect
requirement:
!type:RoleTimeRequirement
role: JobHeadOfPersonnel
time: 54000 #15 hrs, special reward for HoP mains
# Jumpsuit
- type: loadout
id: HoPJumpsuit
equipment: HoPJumpsuit
- type: startingGear
id: HoPJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitHoP
- type: loadout
id: HoPJumpskirt
equipment: HoPJumpskirt
- type: startingGear
id: HoPJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtHoP
# Head
- type: loadout
id: HoPHead
equipment: HoPHead
- type: startingGear
id: HoPHead
equipment:
head: ClothingHeadHatHopcap
# Neck
- type: loadout
id: HoPCloak
equipment: HoPCloak
- type: startingGear
id: HoPCloak
equipment:
neck: ClothingNeckCloakHop
- type: loadout
id: HoPMantle
equipment: HoPMantle
- type: startingGear
id: HoPMantle
equipment:
neck: ClothingNeckMantleHOP
# Back
- type: loadout
id: HoPBackpack
equipment: HoPBackpack
- type: startingGear
id: HoPBackpack
equipment:
back: ClothingBackpackHOPFilled
- type: loadout
id: HoPSatchel
equipment: HoPSatchel
- type: startingGear
id: HoPSatchel
equipment:
back: ClothingBackpackSatchelHOPFilled
- type: loadout
id: HoPDuffel
equipment: HoPDuffel
- type: startingGear
id: HoPDuffel
equipment:
back: ClothingBackpackDuffelHOPFilled
- type: loadout
id: HoPBackpackIan
equipment: HoPBackpackIan
effects:
- !type:GroupLoadoutEffect
proto: ProfessionalHoP
- type: startingGear
id: HoPBackpackIan
equipment:
back: ClothingBackpackHOPIanFilled

View File

@@ -0,0 +1,55 @@
# Jumpsuit
- type: loadout
id: AtmosphericTechnicianJumpsuit
equipment: AtmosphericTechnicianJumpsuit
- type: startingGear
id: AtmosphericTechnicianJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitAtmos
- type: loadout
id: AtmosphericTechnicianJumpskirt
equipment: AtmosphericTechnicianJumpskirt
- type: startingGear
id: AtmosphericTechnicianJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtAtmos
- type: loadout
id: AtmosphericTechnicianJumpsuitCasual
equipment: AtmosphericTechnicianJumpsuitCasual
- type: startingGear
id: AtmosphericTechnicianJumpsuitCasual
equipment:
jumpsuit: ClothingUniformJumpsuitAtmosCasual
# Back
- type: loadout
id: AtmosphericTechnicianBackpack
equipment: AtmosphericTechnicianBackpack
- type: startingGear
id: AtmosphericTechnicianBackpack
equipment:
back: ClothingBackpackAtmosphericsFilled
- type: loadout
id: AtmosphericTechnicianSatchel
equipment: AtmosphericTechnicianSatchel
- type: startingGear
id: AtmosphericTechnicianSatchel
equipment:
back: ClothingBackpackSatchelEngineeringFilled
- type: loadout
id: AtmosphericTechnicianDuffel
equipment: AtmosphericTechnicianDuffel
- type: startingGear
id: AtmosphericTechnicianDuffel
equipment:
back: ClothingBackpackDuffelAtmosphericsFilled

View File

@@ -0,0 +1,97 @@
# Jumpsuit
- type: loadout
id: ChiefEngineerJumpsuit
equipment: ChiefEngineerJumpsuit
- type: startingGear
id: ChiefEngineerJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitChiefEngineer
- type: loadout
id: ChiefEngineerJumpskirt
equipment: ChiefEngineerJumpskirt
- type: startingGear
id: ChiefEngineerJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtChiefEngineer
- type: loadout
id: ChiefEngineerTurtleneck
equipment: ChiefEngineerTurtleneck
- type: startingGear
id: ChiefEngineerTurtleneck
equipment:
jumpsuit: ClothingUniformJumpsuitChiefEngineerTurtle
- type: loadout
id: ChiefEngineerTurtleneckSkirt
equipment: ChiefEngineerTurtleneckSkirt
- type: startingGear
id: ChiefEngineerTurtleneckSkirt
equipment:
jumpsuit: ClothingUniformJumpskirtChiefEngineerTurtle
# Head
- type: loadout
id: ChiefEngineerHead
equipment: ChiefEngineerHead
- type: startingGear
id: ChiefEngineerHead
equipment:
head: ClothingHeadHatHardhatWhite
- type: loadout
id: ChiefEngineerBeret
equipment: EngineeringBeret
# Neck
- type: loadout
id: ChiefEngineerCloak
equipment: ChiefEngineerCloak
- type: startingGear
id: ChiefEngineerCloak
equipment:
neck: ClothingNeckCloakCe
- type: loadout
id: ChiefEngineerMantle
equipment: ChiefEngineerMantle
- type: startingGear
id: ChiefEngineerMantle
equipment:
neck: ClothingNeckMantleCE
# Back
- type: loadout
id: ChiefEngineerBackpack
equipment: ChiefEngineerBackpack
- type: startingGear
id: ChiefEngineerBackpack
equipment:
back: ClothingBackpackChiefEngineerFilled
- type: loadout
id: ChiefEngineerSatchel
equipment: ChiefEngineerSatchel
- type: startingGear
id: ChiefEngineerSatchel
equipment:
back: ClothingBackpackSatchelChiefEngineerFilled
- type: loadout
id: ChiefEngineerDuffel
equipment: ChiefEngineerDuffel
- type: startingGear
id: ChiefEngineerDuffel
equipment:
back: ClothingBackpackDuffelChiefEngineerFilled

View File

@@ -0,0 +1,189 @@
# Senior times
- type: loadoutEffectGroup
id: SeniorEngineering
effects:
- !type:JobRequirementLoadoutEffect
requirement:
!type:RoleTimeRequirement
role: JobAtmosphericTechnician
time: 21600 #6 hrs
- !type:JobRequirementLoadoutEffect
requirement:
!type:RoleTimeRequirement
role: JobStationEngineer
time: 21600 #6 hrs
- !type:JobRequirementLoadoutEffect
requirement:
!type:DepartmentTimeRequirement
department: Engineering
time: 216000 # 60 hrs
# Head
- type: loadout
id: StationEngineerHead
equipment: StationEngineerHead
- type: startingGear
id: StationEngineerHead
equipment:
head: ClothingHeadHatHardhatYellow
- type: loadout
id: SeniorEngineerBeret
equipment: EngineeringBeret
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: EngineeringBeret
equipment:
head: ClothingHeadHatBeretEngineering
# Jumpsuit
- type: loadout
id: StationEngineerJumpsuit
equipment: StationEngineerJumpsuit
- type: startingGear
id: StationEngineerJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitEngineering
- type: loadout
id: StationEngineerJumpskirt
equipment: StationEngineerJumpskirt
- type: startingGear
id: StationEngineerJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtEngineering
- type: loadout
id: StationEngineerHazardsuit
equipment: StationEngineerHazardsuit
- type: startingGear
id: StationEngineerHazardsuit
equipment:
jumpsuit: ClothingUniformJumpsuitEngineeringHazard
- type: loadout
id: SeniorEngineerJumpsuit
equipment: SeniorEngineerJumpsuit
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: SeniorEngineerJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitSeniorEngineer
- type: loadout
id: SeniorEngineerJumpskirt
equipment: SeniorEngineerJumpskirt
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: SeniorEngineerJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtSeniorEngineer
# Back
- type: loadout
id: StationEngineerBackpack
equipment: StationEngineerBackpack
- type: startingGear
id: StationEngineerBackpack
equipment:
back: ClothingBackpackEngineeringFilled
- type: loadout
id: StationEngineerSatchel
equipment: StationEngineerSatchel
- type: startingGear
id: StationEngineerSatchel
equipment:
back: ClothingBackpackSatchelEngineeringFilled
- type: loadout
id: StationEngineerDuffel
equipment: StationEngineerDuffel
- type: startingGear
id: StationEngineerDuffel
equipment:
back: ClothingBackpackDuffelEngineeringFilled
- type: loadout
id: SeniorEngineerBackpack
equipment: SeniorEngineerBackpack
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: SeniorEngineerBackpack
equipment:
back: ClothingBackpackEngineeringFilled
- type: loadout
id: SeniorEngineerSatchel
equipment: SeniorEngineerSatchel
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: SeniorEngineerSatchel
equipment:
back: ClothingBackpackSatchelEngineeringFilled
- type: loadout
id: SeniorEngineerDuffel
equipment: SeniorEngineerDuffel
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: SeniorEngineerDuffel
equipment:
back: ClothingBackpackDuffelEngineeringFilled
# OuterClothing
- type: loadout
id: StationEngineerOuterVest
equipment: StationEngineerOuterVest
- type: startingGear
id: StationEngineerOuterVest
equipment:
outerClothing: ClothingOuterVestHazard
# ID
- type: loadout
id: StationEngineerPDA
equipment: StationEngineerPDA
- type: startingGear
id: StationEngineerPDA
equipment:
id: EngineerPDA
- type: loadout
id: SeniorEngineerPDA
equipment: SeniorEngineerPDA
effects:
- !type:GroupLoadoutEffect
proto: SeniorEngineering
- type: startingGear
id: SeniorEngineerPDA
equipment:
id: SeniorEngineerPDA

View File

@@ -0,0 +1,18 @@
# Jumpsuit
- type: loadout
id: YellowJumpsuit
equipment: YellowJumpsuit
- type: startingGear
id: YellowJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitColorYellow
- type: loadout
id: YellowJumpskirt
equipment: YellowJumpskirt
- type: startingGear
id: YellowJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtColorYellow

View File

@@ -0,0 +1,56 @@
# Jumpsuit
- type: loadout
id: ChemistJumpsuit
equipment: ChemistJumpsuit
- type: startingGear
id: ChemistJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitChemistry
- type: loadout
id: ChemistJumpskirt
equipment: ChemistJumpskirt
- type: startingGear
id: ChemistJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtChemistry
# Back
- type: loadout
id: ChemistBackpack
equipment: ChemistBackpack
- type: startingGear
id: ChemistBackpack
equipment:
back: ClothingBackpackChemistryFilled
- type: loadout
id: ChemistSatchel
equipment: ChemistSatchel
- type: startingGear
id: ChemistSatchel
equipment:
back: ClothingBackpackSatchelChemistryFilled
- type: loadout
id: ChemistDuffel
equipment: ChemistDuffel
- type: startingGear
id: ChemistDuffel
equipment:
back: ClothingBackpackDuffelChemistryFilled
# Outer clothing
- type: loadout
id: ChemistLabCoat
equipment: ChemistLabCoat
- type: startingGear
id: ChemistLabCoat
equipment:
outerClothing: ClothingOuterCoatLabChem

View File

@@ -0,0 +1,85 @@
# Jumpsuit
- type: loadout
id: ChiefMedicalOfficerJumpsuit
equipment: ChiefMedicalOfficerJumpsuit
- type: startingGear
id: ChiefMedicalOfficerJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitCMO
- type: loadout
id: ChiefMedicalOfficerJumpskirt
equipment: ChiefMedicalOfficerJumpskirt
- type: startingGear
id: ChiefMedicalOfficerJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtCMO
# Head
- type: loadout
id: ChiefMedicalOfficerBeret
equipment: ChiefMedicalOfficerBeret
- type: startingGear
id: ChiefMedicalOfficerBeret
equipment:
head: ClothingHeadHatBeretCmo
# Neck
- type: loadout
id: ChiefMedicalOfficerCloak
equipment: ChiefMedicalOfficerCloak
- type: startingGear
id: ChiefMedicalOfficerCloak
equipment:
neck: ClothingCloakCmo
- type: loadout
id: ChiefMedicalOfficerMantle
equipment: ChiefMedicalOfficerMantle
- type: startingGear
id: ChiefMedicalOfficerMantle
equipment:
neck: ClothingNeckMantleCMO
# Back
- type: loadout
id: ChiefMedicalOfficerBackpack
equipment: ChiefMedicalOfficerBackpack
- type: startingGear
id: ChiefMedicalOfficerBackpack
equipment:
back: ClothingBackpackCMOFilled
- type: loadout
id: ChiefMedicalOfficerSatchel
equipment: ChiefMedicalOfficerSatchel
- type: startingGear
id: ChiefMedicalOfficerSatchel
equipment:
back: ClothingBackpackSatchelCMOFilled
- type: loadout
id: ChiefMedicalOfficerDuffel
equipment: ChiefMedicalOfficerDuffel
- type: startingGear
id: ChiefMedicalOfficerDuffel
equipment:
back: ClothingBackpackDuffelCMOFilled
# Outer clothing
- type: loadout
id: ChiefMedicalOfficerLabCoat
equipment: ChiefMedicalOfficerLabCoat
- type: startingGear
id: ChiefMedicalOfficerLabCoat
equipment:
outerClothing: ClothingOuterCoatLabCmo

View File

@@ -0,0 +1,239 @@
# Senior Time
- type: loadoutEffectGroup
id: SeniorPhysician
effects:
- !type:JobRequirementLoadoutEffect
requirement:
!type:RoleTimeRequirement
role: JobChemist
time: 21600 #6 hrs
- !type:JobRequirementLoadoutEffect
requirement:
!type:RoleTimeRequirement
role: JobMedicalDoctor
time: 21600 #6 hrs
- !type:JobRequirementLoadoutEffect
requirement:
!type:DepartmentTimeRequirement
department: Medical
time: 216000 # 60 hrs
# Head
- type: loadout
id: SeniorPhysicianBeret
equipment: SeniorPhysicianBeret
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianBeret
equipment:
head: ClothingHeadHatBeretSeniorPhysician
- type: loadout
id: MedicalBeret
equipment: MedicalBeret
- type: startingGear
id: MedicalBeret
equipment:
head: ClothingHeadHatBeretMedic
- type: loadout
id: BlueSurgeryCap
equipment: BlueSurgeryCap
- type: startingGear
id: BlueSurgeryCap
equipment:
head: ClothingHeadHatSurgcapBlue
- type: loadout
id: GreenSurgeryCap
equipment: GreenSurgeryCap
- type: startingGear
id: GreenSurgeryCap
equipment:
head: ClothingHeadHatSurgcapGreen
- type: loadout
id: PurpleSurgeryCap
equipment: PurpleSurgeryCap
- type: startingGear
id: PurpleSurgeryCap
equipment:
head: ClothingHeadHatSurgcapPurple
# Jumpsuit
- type: loadout
id: MedicalDoctorJumpsuit
equipment: MedicalDoctorJumpsuit
- type: startingGear
id: MedicalDoctorJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitMedicalDoctor
- type: loadout
id: MedicalDoctorJumpskirt
equipment: MedicalDoctorJumpskirt
- type: startingGear
id: MedicalDoctorJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtMedicalDoctor
- type: loadout
id: SeniorPhysicianJumpsuit
equipment: SeniorPhysicianJumpsuit
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitSeniorPhysician
- type: loadout
id: SeniorPhysicianJumpskirt
equipment: SeniorPhysicianJumpskirt
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtSeniorPhysician
- type: loadout
id: MedicalBlueScrubs
equipment: MedicalBlueScrubs
- type: startingGear
id: MedicalBlueScrubs
equipment:
jumpsuit: UniformScrubsColorBlue
- type: loadout
id: MedicalGreenScrubs
equipment: MedicalGreenScrubs
- type: startingGear
id: MedicalGreenScrubs
equipment:
jumpsuit: UniformScrubsColorGreen
- type: loadout
id: MedicalPurpleScrubs
equipment: MedicalPurpleScrubs
- type: startingGear
id: MedicalPurpleScrubs
equipment:
jumpsuit: UniformScrubsColorPurple
# Back
- type: loadout
id: MedicalDoctorBackpack
equipment: MedicalDoctorBackpack
- type: startingGear
id: MedicalDoctorBackpack
equipment:
back: ClothingBackpackMedicalFilled
- type: loadout
id: MedicalDoctorSatchel
equipment: MedicalDoctorSatchel
- type: startingGear
id: MedicalDoctorSatchel
equipment:
back: ClothingBackpackSatchelMedicalFilled
- type: loadout
id: MedicalDoctorDuffel
equipment: MedicalDoctorDuffel
- type: startingGear
id: MedicalDoctorDuffel
equipment:
back: ClothingBackpackDuffelMedicalFilled
- type: loadout
id: SeniorPhysicianBackpack
equipment: SeniorPhysicianBackpack
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianBackpack
equipment:
back: ClothingBackpackDuffelMedicalFilled
- type: loadout
id: SeniorPhysicianSatchel
equipment: SeniorPhysicianSatchel
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianSatchel
equipment:
back: ClothingBackpackSatchelMedicalFilled
- type: loadout
id: SeniorPhysicianDuffel
equipment: SeniorPhysicianDuffel
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianDuffel
equipment:
back: ClothingBackpackDuffelMedicalFilled
# OuterClothing
- type: loadout
id: SeniorPhysicianLabCoat
equipment: SeniorPhysicianLabCoat
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianLabCoat
equipment:
outerClothing: ClothingOuterCoatLabSeniorPhysician
# ID
- type: loadout
id: MedicalDoctorPDA
equipment: MedicalDoctorPDA
- type: startingGear
id: MedicalDoctorPDA
equipment:
id: MedicalPDA
- type: loadout
id: SeniorPhysicianPDA
equipment: SeniorPhysicianPDA
effects:
- !type:GroupLoadoutEffect
proto: SeniorPhysician
- type: startingGear
id: SeniorPhysicianPDA
equipment:
id: SeniorPhysicianPDA

View File

@@ -0,0 +1,18 @@
# Jumpsuit
- type: loadout
id: WhiteJumpsuit
equipment: WhiteJumpsuit
- type: startingGear
id: WhiteJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitColorWhite
- type: loadout
id: WhiteJumpskirt
equipment: WhiteJumpskirt
- type: startingGear
id: WhiteJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtColorWhite

View File

@@ -0,0 +1,66 @@
# Head
- type: loadout
id: ParamedicHead
equipment: ParamedicHead
- type: startingGear
id: ParamedicHead
equipment:
head: ClothingHeadHatParamedicsoft
# Jumpsuit
- type: loadout
id: ParamedicJumpsuit
equipment: ParamedicJumpsuit
- type: startingGear
id: ParamedicJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitParamedic
- type: loadout
id: ParamedicJumpskirt
equipment: ParamedicJumpskirt
- type: startingGear
id: ParamedicJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtParamedic
# Back
- type: loadout
id: ParamedicBackpack
equipment: ParamedicBackpack
- type: startingGear
id: ParamedicBackpack
equipment:
back: ClothingBackpackParamedicFilled
- type: loadout
id: ParamedicSatchel
equipment: ParamedicSatchel
- type: startingGear
id: ParamedicSatchel
equipment:
back: ClothingBackpackSatchelParamedicFilled
- type: loadout
id: ParamedicDuffel
equipment: ParamedicDuffel
- type: startingGear
id: ParamedicDuffel
equipment:
back: ClothingBackpackDuffelParamedicFilled
# Outer clothing
- type: loadout
id: ParamedicWindbreaker
equipment: ParamedicWindbreaker
- type: startingGear
id: ParamedicWindbreaker
equipment:
outerClothing: ClothingOuterCoatParamedicWB

View File

@@ -0,0 +1,82 @@
# Head
- type: loadout
id: ResearchDirectorBeret
equipment: ScientificBeret
# Neck
- type: loadout
id: ResearchDirectorMantle
equipment: ResearchDirectorMantle
- type: startingGear
id: ResearchDirectorMantle
equipment:
neck: ClothingNeckMantleRD
- type: loadout
id: ResearchDirectorCloak
equipment: ResearchDirectorCloak
- type: startingGear
id: ResearchDirectorCloak
equipment:
neck: ClothingNeckCloakRd
# Jumpsuit
- type: loadout
id: ResearchDirectorJumpsuit
equipment: ResearchDirectorJumpsuit
- type: startingGear
id: ResearchDirectorJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitResearchDirector
- type: loadout
id: ResearchDirectorJumpskirt
equipment: ResearchDirectorJumpskirt
- type: startingGear
id: ResearchDirectorJumpskirt
equipment:
jumpsuit: ClothingUniformJumpskirtResearchDirector
# Back
- type: loadout
id: ResearchDirectorBackpack
equipment: ResearchDirectorBackpack
- type: startingGear
id: ResearchDirectorBackpack
equipment:
back: ClothingBackpackResearchDirectorFilled
- type: loadout
id: ResearchDirectorSatchel
equipment: ResearchDirectorSatchel
- type: startingGear
id: ResearchDirectorSatchel
equipment:
back: ClothingBackpackSatchelResearchDirectorFilled
- type: loadout
id: ResearchDirectorDuffel
equipment: ResearchDirectorDuffel
- type: startingGear
id: ResearchDirectorDuffel
equipment:
back: ClothingBackpackDuffelResearchDirectorFilled
# OuterClothing
- type: loadout
id: ResearchDirectorLabCoat
equipment: ResearchDirectorLabCoat
- type: startingGear
id: ResearchDirectorLabCoat
equipment:
outerClothing: ClothingOuterCoatRD

Some files were not shown because too many files have changed in this diff Show More