Система навыков (#324)

* skills

* engine

* fix

* fixtests
This commit is contained in:
Zekins
2025-10-19 18:38:40 +03:00
committed by GitHub
parent 8d9a169dfe
commit adc7478264
97 changed files with 8350 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ using System.IO;
using System.Linq;
using System.Numerics;
using Content.Client._WL.Records; // WL-Records
using Content.Client._WL.Skills.Ui; // WL-Skills
using Content.Client.Electrocution;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
@@ -12,6 +13,7 @@ using Content.Client.Players.PlayTimeTracking;
using Content.Client.Sprite;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared._WL.Skills; // WL-Skills
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.Corvax.CCCVars;
@@ -132,6 +134,7 @@ namespace Content.Client.Lobby.UI
// One at a time.
private LoadoutWindow? _loadoutWindow;
private SkillsWindow? _skillsWindow; // WL-Skills
private TTSTab? _ttsTab; // Corvax-TTS
@@ -1135,6 +1138,13 @@ namespace Content.Client.Lobby.UI
_loadoutWindow?.Dispose();
}
// WL-Skills-start
public void RefreshSkills()
{
_skillsWindow?.Dispose();
}
// WL-Skills-end
/// <summary>
/// Reloads the entire dummy entity for preview.
/// </summary>
@@ -1199,6 +1209,7 @@ namespace Content.Client.Lobby.UI
RefreshAntags();
RefreshJobs();
RefreshLoadouts();
RefreshSkills(); // WL-Skills
RefreshSpecies();
RefreshTraits();
RefreshFlavorText();
@@ -1423,6 +1434,17 @@ namespace Content.Client.Lobby.UI
Margin = new Thickness(3f, 3f, 0f, 0f),
};
// WL-Skills-start
var skillsWindowBtn = new Button()
{
Text = Loc.GetString("skill-window"),
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(3f, 3f, 0f, 0f),
ToolTip = Loc.GetString("skill-window-tooltip")
};
// WL-Skills-end
var collection = IoCManager.Instance!;
var protoManager = collection.Resolve<IPrototypeManager>();
@@ -1452,10 +1474,28 @@ namespace Content.Client.Lobby.UI
};
}
// WL-Skills-start
skillsWindowBtn.OnPressed += args =>
{
OpenSkills(job);
};
// WL-Skills-end
_jobPriorities.Add((job.ID, subnameSelector, selector));
jobContainer.AddChild(selector);
jobContainer.AddChild(loadoutWindowBtn);
// WL-Skills-Edit-start
var buttonsContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Right
};
buttonsContainer.AddChild(loadoutWindowBtn);
buttonsContainer.AddChild(skillsWindowBtn);
jobContainer.AddChild(buttonsContainer);
category.AddChild(jobContainer);
// WL-Skills-Edit-end
}
//WL-Changes-end
}
@@ -1523,6 +1563,61 @@ namespace Content.Client.Lobby.UI
UpdateJobPriorities();
}
// WL-Skills-start
private void OpenSkills(JobPrototype? jobProto)
{
_skillsWindow?.Dispose();
_skillsWindow = null;
if (jobProto == null || Profile == null)
return;
JobOverride = jobProto;
var currentSkills = Profile.Skills.GetValueOrDefault(jobProto.ID, new Dictionary<byte, int>());
var defaultSkills = jobProto.DefaultSkills.ToDictionary(
kvp => (byte)kvp.Key,
kvp => kvp.Value
);
var bonusPoints = jobProto.BonusSkillPoints;
var racialBonus = CalculateRacialBonus(Profile.Species, Profile.Age);
var totalPoints = bonusPoints + racialBonus;
_skillsWindow = new SkillsWindow(jobProto.ID, currentSkills, defaultSkills, totalPoints);
_skillsWindow.OnSkillChanged += (jobId, skillKey, newLevel) =>
{
Profile = Profile.WithSkill(jobId, skillKey, newLevel);
SetDirty();
};
_skillsWindow.OnClose += () =>
{
JobOverride = null;
ReloadPreview();
};
_skillsWindow.OpenCenteredLeft();
JobOverride = jobProto;
ReloadPreview();
}
private int CalculateRacialBonus(string species, int age)
{
var bonus = 0;
foreach (var racialBonusProto in _prototypeManager.EnumeratePrototypes<RacialSkillBonusPrototype>())
{
if (racialBonusProto.Species != species)
continue;
bonus = racialBonusProto.GetBonusForAge(age);
break;
}
return bonus;
}
// WL-Skills-end
private void OnFlavorTextChange(string content)
{
if (Profile is null)
@@ -1604,6 +1699,8 @@ namespace Content.Client.Lobby.UI
_loadoutWindow?.Dispose();
_loadoutWindow = null;
_skillsWindow?.Dispose(); // WL-Skills
_skillsWindow = null; // WL-Skills
}
protected override void EnteredTree()
@@ -1623,6 +1720,7 @@ namespace Content.Client.Lobby.UI
{
Profile = Profile?.WithAge(newAge);
ReloadPreview();
RefreshSkills(); // WL-Skills
}
// WL-Height-Start
@@ -1679,6 +1777,7 @@ namespace Content.Client.Lobby.UI
RefreshJobs();
// In case there's species restrictions for loadouts
RefreshLoadouts();
RefreshSkills(); // WL-Skills
UpdateSexControls(); // update sex for new species
UpdateSpeciesGuidebookIcon();
ReloadPreview();

View File

@@ -1,3 +1,5 @@
using System.Linq;
using Content.Client._WL.Skills.Ui; // WL-Skills
using Content.Client._WL.DynamicText.UI; // WL-Chages
using Content.Client.CharacterInfo;
using Content.Client.Gameplay;
@@ -6,6 +8,8 @@ using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Character.Controls;
using Content.Client.UserInterface.Systems.Character.Windows;
using Content.Client.UserInterface.Systems.Objectives.Controls;
using Content.Shared._WL.Skills; // WL-Skills
using Content.Shared._WL.Skills.Components; // WL-Skills
using Content.Shared.Input;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
@@ -19,7 +23,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
using static Content.Client.CharacterInfo.CharacterInfoSystem;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -31,6 +34,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityNetworkManager _entityNetworkManager = default!; // WL-Skills
//WL-Changes-Start
[Dependency] private readonly DynamicTextUIController _dynamicText = default!;
@@ -47,6 +51,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
}
private CharacterWindow? _window;
private SkillsWindow? _skillsWindow; // WL-Skills
private MenuButton? CharacterButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.CharacterButton;
public void OnStateEntered(GameplayState state)
@@ -60,6 +65,10 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
_window.OnOpen += ActivateButton;
//WL-Changes-Start
_window.SkillsButton.OnPressed += OnSkillsButtonPressed;
if (_player.LocalEntity.HasValue && _ent.HasComponent<SkillsComponent>(_player.LocalEntity))
_window.SkillsButton.Disabled = false;
_window.DynamicTextButton.OnPressed += _ =>
{
_dynamicText.OpenWindow();
@@ -267,4 +276,57 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
_window.Open();
}
}
// WL-Skills-start
private void OnSkillsButtonPressed(ButtonEventArgs args)
{
OpenSkillsWindow();
}
private void OpenSkillsWindow()
{
if (_player.LocalEntity is not { } entity)
return;
if (_skillsWindow != null)
{
_skillsWindow.Close();
_skillsWindow = null;
}
var skillsSystem = _ent.System<SharedSkillsSystem>();
if (!_ent.TryGetComponent<SkillsComponent>(entity, out var skillsComp))
return;
var jobId = skillsComp.CurrentJob;
var defaultSkills = skillsSystem.GetDefaultSkillsForJob(jobId);
var totalPoints = skillsSystem.GetTotalPoints(entity, jobId, skillsComp);
var currentSkills = skillsComp.Skills.ToDictionary(
kvp => (byte)kvp.Key,
kvp => kvp.Value
);
var skillsWindow = new SkillsWindow(
jobId ?? "unknown",
currentSkills,
defaultSkills,
totalPoints,
true
);
_skillsWindow = skillsWindow;
skillsWindow.OnSkillChanged += (changedJobId, skillKey, newLevel) =>
{
if (_player.LocalEntity is not { } localEntity)
return;
var skillType = (SkillType)skillKey;
var ev = new SelectSkillPressedEvent(_ent.GetNetEntity(localEntity), skillType, newLevel, changedJobId);
_entityNetworkManager.SendSystemNetworkMessage(ev);
};
skillsWindow.OpenCentered();
}
// WL-Skills-end
}

View File

@@ -18,6 +18,9 @@
<!--WL-Change-End-->
</BoxContainer>
</BoxContainer>
<!-- WL-Skills-start -->
<Button Name="SkillsButton" Access="Public" Text="{Loc 'character-info-skills-button'}" Disabled="True" Margin="0 10 0 5" HorizontalAlignment="Center" MinWidth="200"/>
<!-- WL-Skills-end -->
<Label Name="ObjectivesLabel" Access="Public" Text="{Loc 'character-info-objectives-label'}" HorizontalAlignment="Center"/>
<BoxContainer Orientation="Vertical" Name="Objectives" Access="Public"/>
<cc:Placeholder Name="RolePlaceholder" Access="Public" PlaceholderText="{Loc 'character-info-roles-antagonist-text'}"/>

View File

@@ -0,0 +1,44 @@
using Content.Client.Eui;
using Content.Shared._WL.Skills.UI;
using Content.Shared.Eui;
using JetBrains.Annotations;
namespace Content.Client._WL.Administration.UI;
[UsedImplicitly]
public sealed class SkillsAdminEui : BaseEui
{
private readonly SkillsAdminWindow _window;
public SkillsAdminEui()
{
_window = new SkillsAdminWindow();
_window.OnClose += () => SendMessage(new SkillsAdminEuiClosedMessage());
_window.OnSkillChanged += (skillKey, newLevel) =>
SendMessage(new SkillsAdminEuiSkillChangedMessage(skillKey, newLevel));
_window.OnPointsChanged += (newBonus) =>
SendMessage(new SkillsAdminEuiPointsChangedMessage(newBonus));
_window.OnResetAll += () => SendMessage(new SkillsAdminEuiResetMessage());
}
public override void Opened()
{
_window.OpenCentered();
}
public override void Closed()
{
_window.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is SkillsAdminEuiState adminState)
{
_window.UpdateState(adminState);
}
}
}

View File

@@ -0,0 +1,74 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="900 750"
Title="{Loc 'skills-admin-window-title'}">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Margin="10">
<!-- Header Info -->
<PanelContainer Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#232323" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
Margin="10 5">
<Label Text="{Loc 'skills-admin-target'}" Margin="10 0" />
<Label Name="TargetNameLabel" HorizontalExpand="True" />
<Label Text="{Loc 'skills-admin-job'}" />
<Label Name="JobLabel" Margin="10 0" />
</BoxContainer>
</PanelContainer>
<!-- Points Control -->
<PanelContainer Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#232323" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" Margin="10 5">
<Label Text="{Loc 'skills-admin-points-control'}"
FontColorOverride="Gold"
HorizontalAlignment="Center" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5">
<Label Text="{Loc 'skills-admin-bonus-points'}" MinWidth="150" />
<LineEdit Name="BonusPointsEdit"
PlaceHolder="0"
HorizontalExpand="True"
Margin="5 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5">
<Label Text="{Loc 'skills-admin-spent-points'}" MinWidth="150" />
<Label Name="SpentPointsLabel" Text="0" HorizontalExpand="True" Margin="5 0" />
</BoxContainer>
<Button Name="ApplyPointsButton"
Text="{Loc 'skills-admin-apply-points'}"
HorizontalAlignment="Center"
Margin="0 5" />
</BoxContainer>
</PanelContainer>
<!-- Skills List -->
<Label Text="{Loc 'skills-admin-skills-list'}"
FontColorOverride="Gold"
HorizontalAlignment="Center" />
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
Name="SkillsContainer"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Footer -->
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 10">
<Button Name="ResetButton"
Text="{Loc 'skills-admin-reset-all'}"
Margin="5 0" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,94 @@
using Content.Client._WL.Skills.Ui;
using Content.Client.UserInterface.Controls;
using Content.Shared._WL.Skills;
using Content.Shared._WL.Skills.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._WL.Administration.UI;
[GenerateTypedNameReferences]
public sealed partial class SkillsAdminWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly SharedSkillsSystem _skillsSystem;
public event Action<byte, int>? OnSkillChanged;
public event Action<int>? OnPointsChanged;
public event Action? OnResetAll;
private Dictionary<byte, int> _currentSkills = new();
private int _bonusPoints;
private int _spentPoints;
public SkillsAdminWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_skillsSystem = _entMan.System<SharedSkillsSystem>();
CloseButton.OnPressed += _ => Close();
ApplyPointsButton.OnPressed += OnApplyPoints;
ResetButton.OnPressed += _ => OnResetAll?.Invoke();
}
public void UpdateState(SkillsAdminEuiState state)
{
TargetNameLabel.Text = state.EntityName;
JobLabel.Text = state.CurrentJob;
_currentSkills = new Dictionary<byte, int>(state.CurrentSkills);
_bonusPoints = state.BonusPoints;
_spentPoints = state.SpentPoints;
UpdatePointsDisplay();
PopulateSkills();
}
private void UpdatePointsDisplay()
{
BonusPointsEdit.Text = _bonusPoints.ToString();
SpentPointsLabel.Text = _spentPoints.ToString();
}
private void PopulateSkills()
{
SkillsContainer.RemoveAllChildren();
foreach (var skillType in Enum.GetValues<SkillType>())
{
var costs = _skillsSystem.GetSkillCost(skillType);
var color = _skillsSystem.GetSkillColor(skillType);
byte skillKey = (byte)skillType;
var currentLevel = _currentSkills.GetValueOrDefault(skillKey, 1);
var skillSelector = new SkillSelector(skillType, currentLevel, costs, color, 1)
{
Margin = new Thickness(0, 5),
HorizontalExpand = true
};
skillSelector.IsLocked = false;
skillSelector.UpdateAvailability(int.MaxValue, _skillsSystem);
skillSelector.OnSkillLevelChanged += (newLevel) =>
{
_currentSkills[skillKey] = newLevel;
OnSkillChanged?.Invoke(skillKey, newLevel);
};
SkillsContainer.AddChild(skillSelector);
}
}
private void OnApplyPoints(BaseButton.ButtonEventArgs args)
{
if (int.TryParse(BonusPointsEdit.Text, out var newBonus))
{
OnPointsChanged?.Invoke(newBonus);
}
}
}

View File

@@ -0,0 +1,7 @@
using Content.Shared._WL.Skills;
namespace Content.Client._WL.Skills;
public sealed class SkillsSystem : SharedSkillsSystem
{
}

View File

@@ -0,0 +1,21 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 2">
<PanelContainer Name="ColorPanel" VerticalExpand="True" MinWidth="4" Margin="2 0 8 0"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Name="SkillNameLabel" MinWidth="250" VerticalAlignment="Center"/>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" HorizontalAlignment="Right">
<Button Name="Level1Button" MinWidth="125" Margin="2 0"/>
<Button Name="Level2Button" MinWidth="125" Margin="2 0"/>
<Button Name="Level3Button" MinWidth="125" Margin="2 0"/>
<Button Name="Level4Button" MinWidth="125" Margin="2 0"/>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="5 2 0 2">
<Label Name="SkillDescriptionLabel" StyleClasses="LabelSubText" />
<Label Name="SkillCostLabel" StyleClasses="LabelSubText" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,207 @@
using Content.Client.Stylesheets;
using Content.Shared._WL.Skills;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._WL.Skills.Ui;
[GenerateTypedNameReferences]
public sealed partial class SkillSelector : Control
{
public Action<int>? OnSkillLevelChanged;
private readonly int[] _costs;
private int _currentLevel;
private int _defaultLevel;
private bool _isLocked;
private readonly bool _upgradeOnly;
public bool IsLocked
{
get => _isLocked;
set
{
_isLocked = value;
UpdateUI();
}
}
public SkillSelector(SkillType skillType, int currentLevel, int[] costs, Color skillColor,
int defaultLevel = 1, bool upgradeOnly = false)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_currentLevel = currentLevel;
_costs = costs;
_defaultLevel = defaultLevel;
_upgradeOnly = upgradeOnly;
ColorPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = skillColor,
ContentMarginTopOverride = 0,
ContentMarginBottomOverride = 0,
ContentMarginLeftOverride = 0,
ContentMarginRightOverride = 0
};
UpdateButtonTexts();
SkillNameLabel.Text = Loc.GetString($"skill-{skillType.ToString().ToLower()}");
SkillDescriptionLabel.Text = Loc.GetString($"skill-{skillType.ToString().ToLower()}-desc");
UpdateUI();
Level1Button.OnPressed += _ => SetLevel(1);
Level2Button.OnPressed += _ => SetLevel(2);
Level3Button.OnPressed += _ => SetLevel(3);
Level4Button.OnPressed += _ => SetLevel(4);
if (_upgradeOnly)
{
Level1Button.Disabled = true;
Level2Button.Disabled = currentLevel <= 2;
Level3Button.Disabled = currentLevel <= 3;
Level4Button.Disabled = currentLevel <= 4;
}
}
private void UpdateButtonTexts()
{
Level1Button.Text = Loc.GetString("skill-level-1") + $" ({GetDisplayCost(0)})";
Level2Button.Text = Loc.GetString("skill-level-2") + $" ({GetDisplayCost(1)})";
Level3Button.Text = Loc.GetString("skill-level-3") + $" ({GetDisplayCost(2)})";
Level4Button.Text = Loc.GetString("skill-level-4") + $" ({GetDisplayCost(3)})";
}
private string GetDisplayCost(int levelIndex)
{
var level = levelIndex + 1;
if (level <= _defaultLevel)
return "0";
return _costs[levelIndex].ToString();
}
public void UpdateAvailability(int unspentPoints, SharedSkillsSystem skillsSystem)
{
if (_isLocked)
return;
for (int targetLevel = 1; targetLevel <= 4; targetLevel++)
{
var button = GetLevelButton(targetLevel);
if (button == null) continue;
if (targetLevel <= _currentLevel)
{
button.Disabled = false;
continue;
}
var upgradeCost = CalculateTotalUpgradeCost(_currentLevel, targetLevel);
button.Disabled = unspentPoints < upgradeCost;
}
}
private int CalculateTotalUpgradeCost(int fromLevel, int toLevel)
{
var totalCost = 0;
for (int level = fromLevel; level < toLevel; level++)
{
if (level >= _defaultLevel && level < _costs.Length)
totalCost += _costs[level];
}
return totalCost;
}
public int GetPaidSpentCost()
{
if (_currentLevel <= _defaultLevel)
return 0;
return CalculateTotalUpgradeCost(_defaultLevel, _currentLevel);
}
private Button? GetLevelButton(int level)
{
return level switch
{
1 => Level1Button,
2 => Level2Button,
3 => Level3Button,
4 => Level4Button,
_ => null
};
}
private void UpdateUI()
{
UpdateLevelButtons();
Level1Button.Disabled = _isLocked || _currentLevel <= 1;
Level2Button.Disabled = _isLocked || _currentLevel <= 2;
Level3Button.Disabled = _isLocked || _currentLevel <= 3;
Level4Button.Disabled = _isLocked || _currentLevel <= 4;
var paidSpent = GetPaidSpentCost();
if (paidSpent > 0)
{
SkillCostLabel.Text = Loc.GetString("skill-total-cost", ("cost", paidSpent));
}
else if (_currentLevel > _defaultLevel)
{
SkillCostLabel.Text = Loc.GetString("skill-free-upgraded");
}
else
{
SkillCostLabel.Text = Loc.GetString("skill-free-default");
}
if (_isLocked)
{
SkillNameLabel.Modulate = Color.Gray;
SkillDescriptionLabel.Modulate = Color.Gray;
SkillCostLabel.Modulate = Color.Gray;
SkillCostLabel.Text = Loc.GetString("skill-locked-default");
}
else
{
SkillNameLabel.Modulate = Color.White;
SkillDescriptionLabel.Modulate = Color.LightGray;
SkillCostLabel.Modulate = Color.LightGray;
}
}
private void UpdateLevelButtons()
{
Level1Button.Pressed = _currentLevel >= 1;
Level2Button.Pressed = _currentLevel >= 2;
Level3Button.Pressed = _currentLevel >= 3;
Level4Button.Pressed = _currentLevel >= 4;
Level1Button.StyleClasses.Add(StyleNano.ButtonOpenRight);
Level2Button.StyleClasses.Add(StyleNano.ButtonOpenBoth);
Level3Button.StyleClasses.Add(StyleNano.ButtonOpenBoth);
Level4Button.StyleClasses.Add(StyleNano.ButtonOpenLeft);
}
public void SetLevel(int level)
{
if (level == _currentLevel || _isLocked)
return;
if (_upgradeOnly && level < _currentLevel)
return;
_currentLevel = level;
UpdateUI();
OnSkillLevelChanged?.Invoke(level);
}
}

View File

@@ -0,0 +1,50 @@
using System.Numerics;
using Content.Client.Eui;
using Content.Shared._WL.Skills.UI;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.Graphics;
namespace Content.Client._WL.Skills.Ui;
[UsedImplicitly]
public sealed class SkillsEui : BaseEui
{
private readonly SkillsForcedWindow _window;
public SkillsEui()
{
_window = new SkillsForcedWindow();
_window.OnClose += () =>
{
SendMessage(new SkillsEuiClosedMessage());
};
_window.OnSkillChanged += (jobId, skillKey, newLevel) =>
{
SendMessage(new SkillsEuiSkillChangedMessage(jobId, skillKey, newLevel));
};
}
public override void Opened()
{
IoCManager.Resolve<IClyde>().RequestWindowAttention();
_window.OpenCenteredAt(new Vector2(0.5f, 0.75f));
}
public override void Closed()
{
_window.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is SkillsEuiState skillsState)
{
_window.UpdateState(skillsState);
}
}
}

View File

@@ -0,0 +1,10 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="1000 800"
Title="{Loc 'skills-forced-window-title'}">
<BoxContainer Orientation="Vertical"
Name="SkillsContainer"
HorizontalExpand="True"
VerticalExpand="True"
Margin="10"/>
</controls:FancyWindow>

View File

@@ -0,0 +1,181 @@
using Content.Client.UserInterface.Controls;
using Content.Shared._WL.Skills;
using Content.Shared._WL.Skills.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._WL.Skills.Ui;
[GenerateTypedNameReferences]
public sealed partial class SkillsForcedWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly SharedSkillsSystem _skillsSystem;
public event Action<string, byte, int>? OnSkillChanged;
private string _jobId = string.Empty;
private Dictionary<byte, int> _currentSkills = new();
private Dictionary<byte, int> _defaultSkills = new();
private int _totalPoints;
private int _spentPoints;
public SkillsForcedWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_skillsSystem = _entMan.System<SharedSkillsSystem>();
}
public void UpdateState(SkillsEuiState state)
{
_jobId = state.JobId;
_currentSkills = new Dictionary<byte, int>(state.CurrentSkills);
_defaultSkills = new Dictionary<byte, int>(state.DefaultSkills);
_totalPoints = state.TotalPoints;
_spentPoints = state.SpentPoints;
UpdateWindow();
}
private void UpdateWindow()
{
SkillsContainer.RemoveAllChildren();
var pointsLabel = new Label
{
Text = $"{_totalPoints - _spentPoints} / {_totalPoints}",
HorizontalAlignment = Control.HAlignment.Center,
Margin = new Thickness(0, 0, 0, 10)
};
SkillsContainer.AddChild(pointsLabel);
var warningLabel = new Label
{
Text = Loc.GetString("skills-forced-warning"),
StyleClasses = { "LabelSubText" },
HorizontalAlignment = Control.HAlignment.Center,
Margin = new Thickness(0, 0, 0, 20)
};
SkillsContainer.AddChild(warningLabel);
var scrollContainer = new ScrollContainer
{
VerticalExpand = true
};
var skillsList = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true
};
scrollContainer.AddChild(skillsList);
PopulateSkillsList(skillsList);
SkillsContainer.AddChild(scrollContainer);
var bottomContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalAlignment = Control.HAlignment.Center,
Margin = new Thickness(0, 20, 0, 0)
};
var confirmButton = new Button
{
Text = Loc.GetString("skills-confirm-button"),
HorizontalAlignment = Control.HAlignment.Center
};
var warningContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalAlignment = Control.HAlignment.Center
};
confirmButton.OnPressed += _ =>
{
if (_totalPoints - _spentPoints == 0)
{
Close();
}
else
{
warningContainer.RemoveAllChildren();
var warning = new Label
{
Text = Loc.GetString("skills-unspent-warning"),
FontColorOverride = Color.Yellow,
HorizontalAlignment = Control.HAlignment.Center
};
warningContainer.AddChild(warning);
}
};
bottomContainer.AddChild(confirmButton);
bottomContainer.AddChild(warningContainer);
SkillsContainer.AddChild(bottomContainer);
}
private void PopulateSkillsList(BoxContainer skillsList)
{
var unspentPoints = _totalPoints - _spentPoints;
foreach (var skillType in Enum.GetValues<SkillType>())
{
var costs = _skillsSystem.GetSkillCost(skillType);
var color = _skillsSystem.GetSkillColor(skillType);
byte skillKey = (byte)skillType;
var defaultLevel = _defaultSkills.GetValueOrDefault(skillKey, 1);
var currentLevel = _currentSkills.GetValueOrDefault(skillKey, defaultLevel);
var skillSelector = new SkillSelector(skillType, currentLevel, costs, color, defaultLevel)
{
Margin = new Thickness(0, 5)
};
skillSelector.IsLocked = defaultLevel == 4;
skillSelector.UpdateAvailability(unspentPoints, _skillsSystem);
skillSelector.OnSkillLevelChanged += (newLevel) =>
{
if (newLevel < defaultLevel)
{
skillSelector.SetLevel(currentLevel);
return;
}
_currentSkills[skillKey] = newLevel;
OnSkillChanged?.Invoke(_jobId, skillKey, newLevel);
_spentPoints = CalculateTotalSpentPoints();
UpdateWindow();
};
skillsList.AddChild(skillSelector);
}
}
private int CalculateTotalSpentPoints()
{
var totalSpent = 0;
foreach (var (skillKey, level) in _currentSkills)
{
var skillType = (SkillType)skillKey;
var costs = _skillsSystem.GetSkillCost(skillType);
var defaultLevel = _defaultSkills.GetValueOrDefault(skillKey, 1);
for (int currentLevel = defaultLevel; currentLevel < level; currentLevel++)
{
if (currentLevel < costs.Length)
totalSpent += costs[currentLevel];
}
}
return totalSpent;
}
}

View File

@@ -0,0 +1,12 @@
<controls:FancyWindow xmlns="https://spacestation14.io" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="900 750" Title="{Loc 'skill-window'}" Margin="5 5">
<BoxContainer Orientation="Vertical" Name="SkillsContainer" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 10" HorizontalAlignment="Center">
<Label Text="{Loc 'skill-available-points'}" />
<Label Name="PointsLabel" Text="0" />
</BoxContainer>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical" Name="SkillsList" HorizontalExpand="True"/>
</ScrollContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,151 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared._WL.Skills;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._WL.Skills.Ui;
[GenerateTypedNameReferences]
public sealed partial class SkillsWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly SharedSkillsSystem _skillsSystem;
public event Action<string, byte, int>? OnSkillChanged;
private readonly string _jobId;
private readonly Dictionary<byte, int> _currentSkills;
private readonly Dictionary<byte, int> _defaultSkills;
private readonly bool _upgradeOnly;
private readonly int _totalPoints;
private int _spentPoints;
private readonly Color _rowColor1 = Color.FromHex("#1B1B1E");
private readonly Color _rowColor2 = Color.FromHex("#202025");
private int _rowCount = 0;
public SkillsWindow(
string jobId,
Dictionary<byte, int> currentSkills,
Dictionary<byte, int> defaultSkills,
int totalPoints, bool upgradeOnly = false)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_skillsSystem = _entMan.System<SharedSkillsSystem>();
_jobId = jobId;
_currentSkills = new Dictionary<byte, int>(currentSkills);
_defaultSkills = new Dictionary<byte, int>(defaultSkills);
_upgradeOnly = upgradeOnly;
_totalPoints = totalPoints;
_spentPoints = 0;
PopulateSkills();
UpdatePoints();
}
private void PopulateSkills()
{
SkillsList.DisposeAllChildren();
_rowCount = 0;
foreach (var skillType in Enum.GetValues<SkillType>())
{
var costs = _skillsSystem.GetSkillCost(skillType);
var color = _skillsSystem.GetSkillColor(skillType);
byte skillKey = (byte)skillType;
var defaultLevel = _defaultSkills.GetValueOrDefault(skillKey, 1);
var currentLevel = _currentSkills.GetValueOrDefault(skillKey, defaultLevel);
var skillSelector = new SkillSelector(skillType, currentLevel, costs, color, defaultLevel, _upgradeOnly)
{
IsLocked = _upgradeOnly && defaultLevel == 4
};
skillSelector.OnSkillLevelChanged += (newLevel) =>
{
if (newLevel < defaultLevel)
{
skillSelector.SetLevel(currentLevel);
return;
}
_currentSkills[skillKey] = newLevel;
OnSkillChanged?.Invoke(_jobId, skillKey, newLevel);
UpdatePoints();
};
var rowContainer = CreateSkillRow(skillSelector);
SkillsList.AddChild(rowContainer);
_rowCount++;
}
}
private PanelContainer CreateSkillRow(SkillSelector skillSelector)
{
var currentRowColor = (_rowCount % 2 == 0) ? _rowColor1 : _rowColor2;
return new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = currentRowColor,
ContentMarginTopOverride = 2,
ContentMarginBottomOverride = 2,
ContentMarginLeftOverride = 4,
ContentMarginRightOverride = 4
},
Children = { skillSelector }
};
}
private void UpdatePoints()
{
_spentPoints = CalculateTotalSpentPoints();
var unspentPoints = _totalPoints - _spentPoints;
PointsLabel.Text = $" {unspentPoints} / {_totalPoints}";
UpdateSkillAvailability(unspentPoints);
}
private int CalculateTotalSpentPoints()
{
var totalSpent = 0;
foreach (var (skillKey, level) in _currentSkills)
{
var skillType = (SkillType)skillKey;
var costs = _skillsSystem.GetSkillCost(skillType);
var defaultLevel = _defaultSkills.GetValueOrDefault(skillKey, 1);
for (int currentLevel = defaultLevel; currentLevel < level; currentLevel++)
{
if (currentLevel < costs.Length)
totalSpent += costs[currentLevel];
}
}
return totalSpent;
}
private void UpdateSkillAvailability(int unspentPoints)
{
foreach (var child in SkillsList.Children)
{
if (child is PanelContainer panel && panel.ChildCount > 0)
{
if (panel.Children.ElementAt(0) is SkillSelector selector)
{
selector.UpdateAvailability(unspentPoints, _skillsSystem);
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class Skills : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "profile_job_skills",
columns: table => new
{
profile_job_skills_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_id = table.Column<int>(type: "integer", nullable: false),
job_name = table.Column<string>(type: "text", nullable: false),
skills = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_job_skills", x => x.profile_job_skills_id);
table.ForeignKey(
name: "FK_profile_job_skills_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_job_skills_profile_id_job_name",
table: "profile_job_skills",
columns: new[] { "profile_id", "job_name" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "profile_job_skills");
}
}
}

View File

@@ -1040,6 +1040,38 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileJobSkills", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_job_skills_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("job_name");
b.Property<int>("ProfileId")
.HasColumnType("integer")
.HasColumnName("profile_id");
b.Property<string>("Skills")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("skills");
b.HasKey("Id")
.HasName("PK_profile_job_skills");
b.HasIndex("ProfileId", "JobName")
.IsUnique();
b.ToTable("profile_job_skills", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
@@ -1901,6 +1933,18 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileJobSkills", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("JobSkills")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_job_skills_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
@@ -2218,6 +2262,8 @@ namespace Content.Server.Database.Migrations.Postgres
{
b.Navigation("Antags");
b.Navigation("JobSkills");
b.Navigation("JobSubnames");
b.Navigation("JobUnblockings");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class Skills : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "profile_job_skills",
columns: table => new
{
profile_job_skills_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_id = table.Column<int>(type: "INTEGER", nullable: false),
job_name = table.Column<string>(type: "TEXT", nullable: false),
skills = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_job_skills", x => x.profile_job_skills_id);
table.ForeignKey(
name: "FK_profile_job_skills_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_job_skills_profile_id_job_name",
table: "profile_job_skills",
columns: new[] { "profile_id", "job_name" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "profile_job_skills");
}
}
}

View File

@@ -985,6 +985,36 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileJobSkills", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_job_skills_id");
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("job_name");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER")
.HasColumnName("profile_id");
b.Property<string>("Skills")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("skills");
b.HasKey("Id")
.HasName("PK_profile_job_skills");
b.HasIndex("ProfileId", "JobName")
.IsUnique();
b.ToTable("profile_job_skills", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
@@ -1818,6 +1848,18 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileJobSkills", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("JobSkills")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_job_skills_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
@@ -2135,6 +2177,8 @@ namespace Content.Server.Database.Migrations.Sqlite
{
b.Navigation("Antags");
b.Navigation("JobSkills");
b.Navigation("JobSubnames");
b.Navigation("JobUnblockings");

View File

@@ -9,6 +9,8 @@ using System.Net;
using System.Text.Json;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking; // WL-Skills
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; // WL-Skills
using NpgsqlTypes;
namespace Content.Server.Database
@@ -46,6 +48,7 @@ namespace Content.Server.Database
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
public DbSet<ProfileJobSkills> ProfileJobSkills { get; set; } = null!; // WL-Skills
//WL-Changes-start
public DbSet<DiscordConnection> DiscordConnections { get; set; } = null!;
@@ -123,6 +126,30 @@ namespace Content.Server.Database
modelBuilder.Entity<JobSubname>()
.HasIndex(j => new { j.ProfileId, j.JobName })
.IsUnique();
modelBuilder.Entity<ProfileJobSkills>(entity =>
{
var converter = new ValueConverter<Dictionary<byte, int>, string>(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null!),
v => JsonSerializer.Deserialize<Dictionary<byte, int>>(v, (JsonSerializerOptions)null!) ?? new());
var comparer = new ValueComparer<Dictionary<byte, int>>(
(l, r) => l != null && r != null && l.SequenceEqual(r),
v => v.Aggregate(0, (a, p) => HashCode.Combine(a, p.Key.GetHashCode(), p.Value.GetHashCode())),
v => v.ToDictionary(kv => kv.Key, kv => kv.Value));
entity.Property(e => e.Skills)
.HasConversion(converter)
.Metadata.SetValueComparer(comparer);
entity.HasIndex(p => new { p.ProfileId, p.JobName })
.IsUnique();
entity.HasOne(e => e.Profile)
.WithMany(e => e.JobSkills)
.HasForeignKey(e => e.ProfileId)
.IsRequired();
});
//WL-Changes-end
modelBuilder.Entity<AssignedUserId>()
@@ -455,6 +482,7 @@ namespace Content.Server.Database
public List<Antag> Antags { get; } = new();
public List<Trait> Traits { get; } = new();
public List<JobSubname> JobSubnames { get; } = new(); //WL-Subnames
public List<ProfileJobSkills> JobSkills { get; } = new(); // WL-Skills
public List<JobUnblocking> JobUnblockings { get; } = new(); //WL-Changes
public List<ProfileRoleLoadout> Loadouts { get; } = new();
@@ -606,10 +634,29 @@ namespace Content.Server.Database
/*
* Insert extra data here like custom descriptions or colors or whatever.
*/
}
}
#endregion
// WL-Skills-start
#region Job Skills
public class ProfileJobSkills
{
public int Id { get; set; }
public int ProfileId { get; set; }
public Profile Profile { get; set; } = null!;
public string JobName { get; set; } = string.Empty;
[Column(TypeName = "jsonb")]
public Dictionary<byte, int> Skills { get; set; } = new();
}
#endregion
// WL-Skills-end
public enum DbPreferenceUnavailableMode
{
// These enum values HAVE to match the ones in PreferenceUnavailableMode in Shared.

View File

@@ -41,6 +41,7 @@ using System.Linq;
using static Content.Shared.Configurable.ConfigurationComponent;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Humanoid;
using Content.Shared._WL.Skills.Components; // Wl-Skills
namespace Content.Server.Administration.Systems
@@ -657,6 +658,26 @@ namespace Content.Server.Administration.Systems
args.Verbs.Add(verb);
}
// WL-Changes: Languages end
// Wl-Skills-start
// Skills Management Verb
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin) && EntityManager.HasComponent<SkillsComponent>(args.Target))
{
args.Verbs.Add(new Verb
{
Text = Loc.GetString("skills-admin-verb-manage"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
Act = () =>
{
var eui = new SkillsAdminEui(args.Target);
_euiManager.OpenEui(eui, player);
eui.StateDirty();
},
Impact = LogImpact.Medium
});
}
// Wl-Skills-end
}
#region SolutionsEui

View File

@@ -17,6 +17,8 @@ using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared._WL.Skills.Components; // WL-Skills
using Content.Shared._WL.Skills; // WL-Skills
namespace Content.Server.Cloning;
@@ -36,6 +38,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly SharedSkillsSystem _skills = default!; // WL-Skills
/// <summary>
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
@@ -62,6 +65,8 @@ public sealed partial class CloningSystem : SharedCloningSystem
CloneComponents(original, clone.Value, settings);
CopySkills(original, clone.Value); // WL-Skills
// Add equipment first so that SetEntityName also renames the ID card.
if (settings.CopyEquipment != null)
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
@@ -130,6 +135,16 @@ public sealed partial class CloningSystem : SharedCloningSystem
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied using CopyComp
}
// WL-Skills-start
/// <summary>
/// Copies all skills from the original entity to the clone.
/// </summary>
public void CopySkills(EntityUid original, EntityUid clone)
{
_skills.CopySkills(original, clone);
}
// WL-Skills-end
/// <summary>
/// Copies the equipment the original has to the clone.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!

View File

@@ -106,6 +106,7 @@ namespace Content.Server.Database
//WL-Changes-start
.Include(p => p.Profiles).ThenInclude(h => h.JobSubnames)
.Include(p => p.Profiles).ThenInclude(h => h.JobUnblockings)
.Include(p => p.Profiles).ThenInclude(h => h.JobSkills)
//WL-Changes-end
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
.Include(p => p.Profiles).ThenInclude(h => h.Traits)
@@ -166,6 +167,7 @@ namespace Content.Server.Database
//WL-Changes-start
.Include(p => p.JobSubnames)
.Include(p => p.JobUnblockings)
.Include(p => p.JobSkills)
//WL-Changes-end
.Include(p => p.Antags)
.Include(p => p.Traits)
@@ -272,6 +274,7 @@ namespace Content.Server.Database
//WL-Changes-start
var jobSubnames = profile.JobSubnames.ToDictionary(x => x.JobName, x => x.Subname);
var jobUnblockings = profile.JobUnblockings.ToDictionary(k => k.JobName, v => v.ForceUnblocked);
var jobSkills = profile.JobSkills.ToDictionary(js => js.JobName, js => js.Skills);
//WL-Changes-end
var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
@@ -364,7 +367,8 @@ namespace Content.Server.Database
jobUnblockings,
profile.MedicalRecord, // WL-Records
profile.SecurityRecord, // WL-Records
profile.EmploymentRecord // WL-Records
profile.EmploymentRecord, // WL-Records
jobSkills // WL-Skills
);
}
@@ -400,7 +404,18 @@ namespace Content.Server.Database
profile.SpawnPriority = (int) humanoid.SpawnPriority;
profile.Markings = markings;
profile.Slot = slot;
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
profile.PreferenceUnavailable = (DbPreferenceUnavailableMode)humanoid.PreferenceUnavailable;
// WL-Skills-start
profile.JobSkills.Clear();
foreach (var jobSkill in humanoid.Skills)
{
profile.JobSkills.Add(new ProfileJobSkills
{
JobName = jobSkill.Key,
Skills = jobSkill.Value
});
}
// WL-Skills-end
profile.Jobs.Clear();
profile.Jobs.AddRange(

View File

@@ -0,0 +1,138 @@
using System.Linq;
using Content.Server.EUI;
using Content.Server.Prayer;
using Content.Shared._WL.Skills;
using Content.Shared._WL.Skills.Components;
using Content.Shared._WL.Skills.UI;
using Content.Shared.Eui;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Systems;
public sealed class SkillsAdminEui : BaseEui
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly EntityUid _targetEntity;
private readonly SharedSkillsSystem _skillsSystem;
private readonly PrayerSystem _prayer;
private SkillsComponent? _skillsComp;
public SkillsAdminEui(EntityUid targetEntity)
{
IoCManager.InjectDependencies(this);
_targetEntity = targetEntity;
_skillsSystem = _entMan.System<SharedSkillsSystem>();
_prayer = _entMan.System<PrayerSystem>();
_entMan.TryGetComponent(_targetEntity, out _skillsComp);
}
public override EuiStateBase GetNewState()
{
if (_skillsComp == null)
return new SkillsAdminEuiState(false, new(), 0, 0, "", GetEntityName(_targetEntity));
var currentSkills = _skillsComp.Skills.ToDictionary(
kvp => (byte)kvp.Key,
kvp => kvp.Value
);
var defaultSkills = _skillsSystem.GetDefaultSkillsForJob(_skillsComp.CurrentJob);
var jobName = Loc.GetString("skills-admin-skills-no-job");
if (_proto.TryIndex(_skillsComp.CurrentJob, out var jobPrototype))
jobName = jobPrototype.LocalizedName;
return new SkillsAdminEuiState(
true,
currentSkills,
_skillsComp.SpentPoints,
_skillsComp.BonusPoints,
jobName,
GetEntityName(_targetEntity)
);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case SkillsAdminEuiClosedMessage:
Close();
break;
case SkillsAdminEuiSkillChangedMessage changedMsg:
HandleSkillChanged(changedMsg);
break;
case SkillsAdminEuiPointsChangedMessage pointsMsg:
HandlePointsChanged(pointsMsg);
break;
case SkillsAdminEuiResetMessage:
HandleResetAll();
break;
}
}
private void HandleSkillChanged(SkillsAdminEuiSkillChangedMessage message)
{
if (_skillsComp == null)
return;
var defaultSkills = _skillsSystem.ConvertToSkillTypeDict(_skillsSystem.GetDefaultSkillsForJob(_skillsComp.CurrentJob));
var skillType = (SkillType)message.SkillKey;
_skillsSystem.SetSkillLevelAdmin(_targetEntity, skillType, message.NewLevel, defaultSkills, _skillsComp);
if (_entMan.TryGetComponent<ActorComponent>(_targetEntity, out var actor))
_prayer.SendSubtleMessage(actor.PlayerSession, actor.PlayerSession, string.Empty,
Loc.GetString("skills-admin-notify-skills-changed"));
StateDirty();
}
private void HandlePointsChanged(SkillsAdminEuiPointsChangedMessage message)
{
if (_skillsComp == null)
return;
var totalBonusPoints = _skillsComp.BonusPoints;
_skillsSystem.SetBonusPoints(_targetEntity, message.NewBonusPoints, _skillsComp);
if (_entMan.TryGetComponent<ActorComponent>(_targetEntity, out var actor))
{
var messageKey = message.NewBonusPoints > totalBonusPoints
? "skills-admin-notify-points-added" : "skills-admin-notify-points-removed";
_prayer.SendSubtleMessage(actor.PlayerSession, actor.PlayerSession, string.Empty,
Loc.GetString(messageKey));
}
StateDirty();
}
private void HandleResetAll()
{
if (_skillsComp == null)
return;
_skillsSystem.ResetAllSkills(_targetEntity, _skillsComp);
if (_entMan.TryGetComponent<ActorComponent>(_targetEntity, out var actor))
_prayer.SendSubtleMessage(actor.PlayerSession, actor.PlayerSession, string.Empty,
Loc.GetString("skills-admin-notify-skills-reset"));
StateDirty();
}
private string GetEntityName(EntityUid entity)
{
return _entMan.GetComponent<MetaDataComponent>(entity).EntityName;
}
}

View File

@@ -2,7 +2,7 @@ using Content.Server._WL.Ert.Prototypes;
using Content.Server.Shuttles.Components;
using Content.Shared._WL.Entity.Extensions;
using Content.Shared._WL.Ert;
using Content.Shared._WL.Math.Extensions;
using Content.Shared._WL.Mathemathics.Extensions;
using Content.Shared._WL.Random.Extensions;
using JetBrains.Annotations;
using Robust.Server.GameObjects;

View File

@@ -0,0 +1,69 @@
using System.Linq;
using Content.Server.EUI;
using Content.Shared._WL.Skills;
using Content.Shared._WL.Skills.Components;
using Content.Shared._WL.Skills.UI;
using Content.Shared.Eui;
namespace Content.Server._WL.Skills.UI;
public sealed class SkillsEui : BaseEui
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly EntityUid _entity;
private readonly SharedSkillsSystem _skillsSystem;
private readonly string _jobId;
public SkillsEui(EntityUid entity, SharedSkillsSystem skillsSystem, string jobId)
{
IoCManager.InjectDependencies(this);
_entity = entity;
_skillsSystem = skillsSystem;
_jobId = jobId;
}
public override EuiStateBase GetNewState()
{
if (!_entMan.TryGetComponent<SkillsComponent>(_entity, out var skillsComp))
return new SkillsEuiState(_jobId, new(), new(), 0, 0);
var currentSkills = skillsComp.Skills.ToDictionary(
kvp => (byte)kvp.Key,
kvp => kvp.Value
);
var defaultSkills = _skillsSystem.GetDefaultSkillsForJob(_jobId);
return new SkillsEuiState(_jobId, currentSkills, defaultSkills,
_skillsSystem.GetTotalPoints(_entity, _jobId, skillsComp), skillsComp.SpentPoints);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case SkillsEuiClosedMessage:
Close();
break;
case SkillsEuiSkillChangedMessage changedMsg:
HandleSkillChanged(changedMsg);
break;
}
}
private void HandleSkillChanged(SkillsEuiSkillChangedMessage message)
{
if (!_entMan.TryGetComponent<SkillsComponent>(_entity, out var skillsComp))
return;
var skillType = (SkillType)message.SkillKey;
_skillsSystem.TrySetSkillLevel(_entity, skillType, message.NewLevel, _jobId, skillsComp);
StateDirty();
}
}

View File

@@ -0,0 +1,75 @@
using Content.Server._WL.Skills.UI;
using Content.Server.EUI;
using Content.Shared._WL.Skills;
using Content.Shared._WL.Skills.Components;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Roles.Components;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
namespace Content.Server._WL.Skills;
public sealed partial class SkillsSystem : SharedSkillsSystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<SelectSkillPressedEvent>(OnSelectSkill);
SubscribeLocalEvent<SkillsComponent, SkillsAddedEvent>(OnSkillsAdded);
}
private void OnSelectSkill(SelectSkillPressedEvent args)
{
TrySetSkillLevel(GetEntity(args.Uid), args.Skill, args.TargetLevel, args.JobId);
}
private void OnSkillsAdded(EntityUid uid, SkillsComponent component, SkillsAddedEvent args)
{
// Disable AutoOpening for Development
if (!_cfg.GetCVar(CCVars.GameLobbyEnabled))
return;
if (!_mind.TryGetMind(uid, out _, out var mind) || mind is { UserId: null }
|| !_player.TryGetSessionById(mind.UserId, out var session))
return;
var jobId = GetJobIdFromEntity(mind);
if (ShouldForceSkillsSelection(uid, jobId, component))
{
OpenForcedSkillsMenu(session, uid, jobId);
}
}
private string? GetJobIdFromEntity(MindComponent mind)
{
foreach (var roleId in mind.MindRoleContainer.ContainedEntities)
{
if (!TryComp<MindRoleComponent>(roleId, out var role))
continue;
if (role.JobPrototype != null)
{
return role.JobPrototype.Value;
}
}
return null;
}
public void OpenForcedSkillsMenu(ICommonSession player, EntityUid entity, string? jobId)
{
jobId ??= "unknown";
var eui = new SkillsEui(entity, this, jobId);
_eui.OpenEui(eui, player);
eui.StateDirty();
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Text.RegularExpressions;
using Content.Shared._WL.Skills; // WL-Skills
using Content.Shared.CCVar;
using Content.Shared.Corvax.TTS;
using Content.Shared.GameTicking;
@@ -164,7 +165,8 @@ namespace Content.Shared.Preferences
Dictionary<string, bool> jobUnblockings,
string medicalRecord, // WL-Records
string securityRecord, // WL-Records
string employmentRecord // WL-Records
string employmentRecord, // WL-Records
Dictionary<string, Dictionary<byte, int>> skills // WL-Skills
//WL-Changes-end
)
{
@@ -193,6 +195,7 @@ namespace Content.Shared.Preferences
MedicalRecord = medicalRecord; // WL-Records
SecurityRecord = securityRecord; // WL-Records
EmploymentRecord = employmentRecord; // WL-Records
Skills = skills; // WL-Skills
//WL-Changes-end
var hasHighPrority = false;
@@ -233,7 +236,8 @@ namespace Content.Shared.Preferences
new(other.JobUnblockings), // WL-Heigh
other.MedicalRecord, // WL-Records
other.SecurityRecord, // WL-Records
other.EmploymentRecord) // WL-Records
other.EmploymentRecord, // WL-Records
other.Skills) // WL-Skills
{
}
@@ -341,6 +345,8 @@ namespace Content.Shared.Preferences
[DataField("height")] public int Height { get; private set; } = 165; // WL-Height
public IReadOnlyDictionary<string, string> JobSubnames => _jobSubnames; //WL-changes
public IReadOnlyDictionary<string, bool> JobUnblockings => _jobUnblockings;
[DataField] public Dictionary<string, Dictionary<byte, int>> Skills { get; set; } = new(); // WL-Skills
//WL-Changes-end
public HumanoidCharacterProfile WithName(string name)
@@ -469,6 +475,22 @@ namespace Content.Shared.Preferences
_jobUnblockings = dict
};
}
public HumanoidCharacterProfile WithSkill(string jobName, byte skillKey, int level)
{
var newSkills = new Dictionary<string, Dictionary<byte, int>>(Skills);
if (!newSkills.TryGetValue(jobName, out var jobSkills))
{
jobSkills = new Dictionary<byte, int>();
newSkills[jobName] = jobSkills;
}
var newJobSkills = new Dictionary<byte, int>(jobSkills);
newJobSkills[skillKey] = level;
newSkills[jobName] = newJobSkills;
return new(this) { Skills = newSkills };
}
//WL-Changes-end
public HumanoidCharacterProfile WithJobPriority(ProtoId<JobPrototype> jobId, JobPriority priority)
@@ -620,6 +642,24 @@ namespace Content.Shared.Preferences
if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
if (!_jobSubnames.SequenceEqual(other._jobSubnames)) return false; // WL-JobSubnames
if (!_jobUnblockings.SequenceEqual(other._jobUnblockings)) return false; // WL-Changes
// WL-Skills-start
if (Skills.Count != other.Skills.Count) return false;
foreach (var kv in Skills)
{
if (!other.Skills.TryGetValue(kv.Key, out var otherJobSkills))
return false;
var jobSkills = kv.Value;
if (jobSkills.Count != otherJobSkills.Count)
return false;
foreach (var inner in jobSkills)
{
if (!otherJobSkills.TryGetValue(inner.Key, out var otherLevel) || otherLevel != inner.Value)
return false;
}
}
// WL-Skills-end
return Appearance.MemberwiseEquals(other.Appearance);
}
@@ -770,6 +810,47 @@ namespace Content.Shared.Preferences
.Where(prototypeManager.HasIndex)
.ToList();
// WL-Skills-Start
var validSkills = new Dictionary<string, Dictionary<byte, int>>();
foreach (var (jobName, jobSkillDict) in Skills)
{
var validJobSkills = new Dictionary<byte, int>();
foreach (var (skillByte, level) in jobSkillDict)
{
if (Enum.IsDefined(typeof(SkillType), skillByte))
{
var validLevel = Math.Clamp(level, 1, 4);
validJobSkills[skillByte] = validLevel;
}
}
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
var skillByte = (byte)skill;
if (!validJobSkills.ContainsKey(skillByte))
{
validJobSkills[skillByte] = 1;
}
}
validSkills[jobName] = validJobSkills;
}
if (validSkills.Count == 0)
{
var defaultSkills = new Dictionary<byte, int>();
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
defaultSkills[(byte)skill] = 1;
}
foreach (var job in _jobPriorities.Keys)
{
validSkills[job.Id] = new Dictionary<byte, int>(defaultSkills);
}
}
// WL-Skills-End
Name = name;
FlavorText = flavortext;
OocText = oocText; // WL-OOCText
@@ -782,6 +863,7 @@ namespace Content.Shared.Preferences
Gender = gender;
Appearance = appearance;
SpawnPriority = spawnPriority;
Skills = validSkills; // WL-Skills
_jobPriorities.Clear();
@@ -926,6 +1008,20 @@ namespace Content.Shared.Preferences
hashCode.Add(MedicalRecord);
hashCode.Add(SecurityRecord);
hashCode.Add(EmploymentRecord);
unchecked
{
var skillsHash = 0;
foreach (var jobKv in Skills.OrderBy(k => k.Key))
{
var innerHash = 0;
foreach (var sk in jobKv.Value.OrderBy(k => k.Key))
{
innerHash = HashCode.Combine(innerHash, sk.Key.GetHashCode(), sk.Value.GetHashCode());
}
skillsHash = HashCode.Combine(skillsHash, jobKv.Key.GetHashCode(), innerHash);
}
hashCode.Add(skillsHash);
}
//WL-Changes-end
return hashCode.ToHashCode();

View File

@@ -1,3 +1,4 @@
using Content.Shared._WL.Skills; // WL-Skills
using Content.Shared.Access;
using Content.Shared.Guidebook;
using Content.Shared.Players.PlayTimeTracking;
@@ -61,6 +62,14 @@ namespace Content.Shared.Roles
return list;
}
// WL-Skills-start
[DataField("defaultSkills")]
public Dictionary<SkillType, int> DefaultSkills { get; private set; } = new();
[DataField("bonusSkillPoints")]
public int BonusSkillPoints { get; private set; } = 0;
// WL-Skills-end
// WL-Changes-end
/// <summary>

View File

@@ -145,4 +145,13 @@ public sealed class WLCVars
public static readonly CVarDef<int> MaxDynamicTextLength =
CVarDef.Create("ic.dynamic_text_length", 1024, CVar.SERVER | CVar.REPLICATED);
/*
* Skills
*/
/// <summary>
/// ГАНС! ЕСЛИ ОНИ ВДРУГ НЕ НУЖНЫ ТО ПЕРЕКЛЮЧИ ПЕРЕКЛЮЧАТЕЛЬ!!!
/// </summary>
public static readonly CVarDef<bool> SkillsEnabled =
CVarDef.Create("skills.enabled", true, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
}

View File

@@ -1,7 +1,7 @@
using Robust.Shared.Utility;
using static Robust.Shared.Maths.MathHelper;
namespace Content.Shared._WL.Math.Extensions
namespace Content.Shared._WL.Mathemathics.Extensions // Don't name Math this again
{
public static class Box2Ext
{

View File

@@ -0,0 +1,48 @@
namespace Content.Shared._WL.Skills.Components;
[RegisterComponent]
[Access(typeof(SharedSkillsSystem))]
public sealed partial class InitialSkillsComponent : Component
{
/// <summary>
/// Skills that are set to exact levels when the entity is created.
/// </summary>
[DataField("initialSkills")]
public Dictionary<SkillType, int> InitialSkills = new();
/// <summary>
/// Skills that are set to random levels when the entity is created.
/// </summary>
[DataField("randomSkills")]
public List<SkillType> RandomSkills = new();
/// <summary>
/// Skills that are added to existing skills (if entity already has skills component).
/// </summary>
[DataField("addSkills")]
public Dictionary<SkillType, int> AddSkills = new();
/// <summary>
/// Whether to randomize ALL skills when the entity is created.
/// </summary>
[DataField]
public bool RandomizeAllSkills = false;
/// <summary>
/// Whether to override existing skills or add to them.
/// </summary>
[DataField]
public bool OverrideExisting = true;
/// <summary>
/// Minimum level for randomized skills (1-4).
/// </summary>
[DataField]
public int RandomMinLevel = 1;
/// <summary>
/// Maximum level for randomized skills (1-4).
/// </summary>
[DataField]
public int RandomMaxLevel = 4;
}

View File

@@ -0,0 +1,25 @@
using Content.Shared.Roles;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._WL.Skills.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedSkillsSystem))]
public sealed partial class SkillsComponent : Component
{
[DataField("skills"), AutoNetworkedField]
public Dictionary<SkillType, int> Skills = new();
[DataField("unspentPoints"), AutoNetworkedField]
public int UnspentPoints;
[DataField("spentPoints"), AutoNetworkedField]
public int SpentPoints;
[DataField("bonusPoints"), AutoNetworkedField]
public int BonusPoints;
[AutoNetworkedField]
public ProtoId<JobPrototype>? CurrentJob = null;
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared._WL.Skills;
[Prototype("racialSkillBonus")]
public sealed partial class RacialSkillBonusPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("species", customTypeSerializer: typeof(PrototypeIdSerializer<SpeciesPrototype>), required: true)]
public string Species { get; private set; } = default!;
[DataField("ageBonuses")]
public Dictionary<int, int> AgeBonuses { get; private set; } = new();
public int GetBonusForAge(int age)
{
if (AgeBonuses.Count == 0)
return 0;
int maxBonus = 0;
foreach (var (bonusAge, bonus) in AgeBonuses)
{
if (bonusAge <= age)
maxBonus += bonus;
}
return maxBonus;
}
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Prototypes;
namespace Content.Shared._WL.Skills;
[Prototype("skill")]
public sealed partial class SkillPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("skillType", required: true)]
public SkillType SkillType { get; private set; }
[DataField("costs", required: true)]
public int[] Costs { get; private set; } = new[] { 0, 0, 0, 0 };
[DataField("color", required: true)]
public Color Color { get; private set; } = Color.White;
}

View File

@@ -0,0 +1,156 @@
using Content.Shared._WL.CCVars;
using Content.Shared._WL.Skills.Components;
using Robust.Shared.Random;
namespace Content.Shared._WL.Skills;
public abstract partial class SharedSkillsSystem
{
/// <summary>
/// Checks whether the entity has a sufficient skill level to perform the action.
/// </summary>
/// <param name="uid">ID entities</param>
/// <param name="skill">Required skill</param>
/// <param name="requiredLevel">Required minimum level (1-4)</param>
/// <param name="comp">An optional skill component</param>
/// <returns>True if the skill is sufficient</returns>
public bool HasSkill(EntityUid uid, SkillType skill, int requiredLevel = 1, SkillsComponent? comp = null)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return true;
if (!Resolve(uid, ref comp))
return false;
var currentLevel = comp.Skills.GetValueOrDefault(skill, 1);
return currentLevel >= requiredLevel;
}
/// <summary>
/// Checks if the entity has the correct skill level
/// </summary>
public bool HasExactSkill(EntityUid uid, SkillType skill, int exactLevel, SkillsComponent? comp = null)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return true;
if (!Resolve(uid, ref comp))
return false;
var currentLevel = comp.Skills.GetValueOrDefault(skill, 1);
return currentLevel == exactLevel;
}
/// <summary>
/// Gets the current skill level of the entity.
/// </summary>
public int GetSkillLevel(EntityUid uid, SkillType skill, SkillsComponent? comp = null)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return 1;
if (!Resolve(uid, ref comp))
return 1;
return comp.Skills.GetValueOrDefault(skill, 1);
}
/// <summary>
/// Checks whether an entity can perform an action based on the skill's success chance.
/// </summary>
public bool CheckSkillChance(EntityUid uid, SkillType skill, float baseSuccessChance = 0.5f, SkillsComponent? comp = null)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return true;
if (!Resolve(uid, ref comp))
return _random.Prob(baseSuccessChance); // Basic probability if there are no skills
var skillLevel = comp.Skills.GetValueOrDefault(skill, 1);
// Модификатор based on уровня навыка
float skillModifier = skillLevel switch
{
1 => 0.8f, // Beginner - fine
2 => 1.0f, // Amateur - basic
3 => 1.3f, // Specialist - bonus
4 => 1.7f, // Professional - big bonus
_ => 1.0f
};
float successChance = baseSuccessChance * skillModifier;
return _random.Prob(Math.Clamp(successChance, 0f, 1f));
}
/// <summary>
/// Gets an efficiency modifier based on the skill level.
/// </summary>
public float GetSkillEfficiency(EntityUid uid, SkillType skill, SkillsComponent? comp = null)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return 1.0f;
if (!Resolve(uid, ref comp))
return 1.0f; // Basic efficiency
var skillLevel = comp.Skills.GetValueOrDefault(skill, 1);
return skillLevel switch
{
1 => 0.7f, // Beginner - 70% efficiency
2 => 1.0f, // Amateur - 100%
3 => 1.4f, // Specialist - 140%
4 => 1.8f, // Professional - 180%
_ => 1.0f
};
}
/// <summary>
/// Checks whether an entity can perform a complex action (requires multiple skills)
/// </summary>
public bool CanPerformComplexAction(EntityUid uid, SkillsComponent? comp = null, params (SkillType skill, int requiredLevel)[] requirements)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return true;
if (!Resolve(uid, ref comp))
return false; // Can't perform without skills
foreach (var (skill, requiredLevel) in requirements)
{
if (!HasSkill(uid, skill, requiredLevel, comp))
return false;
}
return true;
}
/// <summary>
/// Gets a description of the skill level for the UI.
/// </summary>
public string GetSkillLevelDescription(int level)
{
return level switch
{
1 => Loc.GetString("skill-level-1"),
2 => Loc.GetString("skill-level-2"),
3 => Loc.GetString("skill-level-3"),
4 => Loc.GetString("skill-level-4"),
_ => Loc.GetString("skill-level-unknown")
};
}
/// <summary>
/// Gets a color to display the skill level.
/// </summary>
public Color GetSkillLevelColor(int level)
{
return level switch
{
1 => Color.Gray, // Beginner
2 => Color.Yellow, // Amateur
3 => Color.Blue, // Specialist
4 => Color.Green, // Professional
_ => Color.White
};
}
}

View File

@@ -0,0 +1,569 @@
using Content.Shared._WL.CCVars;
using Content.Shared._WL.Skills.Components;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
namespace Content.Shared._WL.Skills;
public abstract partial class SharedSkillsSystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
#region Initialization
public void InitializeSkills()
{
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);
SubscribeLocalEvent<InitialSkillsComponent, MapInitEvent>(OnMapInit);
}
/// <summary>
/// Initializes skills for a player when they spawn
/// </summary>
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
{
if (args.Profile is not HumanoidCharacterProfile profile || !_cfg.GetCVar(WLCVars.SkillsEnabled))
return;
InitializeSkills(args.Mob, args.JobId, profile);
}
/// <summary>
/// Initializes skills component with default values and applies profile skills
/// </summary>
private void InitializeSkills(EntityUid mob, string? jobId, HumanoidCharacterProfile profile)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return;
var skillsComp = EnsureComp<SkillsComponent>(mob);
skillsComp.Skills.Clear();
skillsComp.CurrentJob = jobId;
int bonusPoints = 0;
Dictionary<SkillType, int> defaultSkills = new();
if (jobId != null && _prototype.TryIndex<JobPrototype>(jobId, out var jobPrototype))
{
defaultSkills = jobPrototype.DefaultSkills;
bonusPoints = jobPrototype.BonusSkillPoints;
}
var racialBonus = CalculateRacialBonus(profile.Species, profile.Age);
var totalPoints = bonusPoints + racialBonus;
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
var defaultLevel = defaultSkills.GetValueOrDefault(skill, 1);
skillsComp.Skills[skill] = defaultLevel;
}
if (jobId != null && profile.Skills.TryGetValue(jobId, out var profileSkills))
{
ApplyProfileSkillsWithLimit(mob, skillsComp, profileSkills, defaultSkills, totalPoints);
}
RecalculateSpentPoints(mob, skillsComp, defaultSkills);
skillsComp.UnspentPoints = Math.Max(0, totalPoints - skillsComp.SpentPoints);
Dirty(mob, skillsComp);
var ev = new SkillsAddedEvent();
RaiseLocalEvent(mob, ref ev);
}
#endregion
#region Skill Calculation Methods
/// <summary>
/// Recalculates the total number of points spent for all skills, excluding default skill costs
/// </summary>
public void RecalculateSpentPoints(EntityUid uid, SkillsComponent? comp = null, Dictionary<SkillType, int>? defaultSkills = null)
{
if (!Resolve(uid, ref comp))
return;
comp.SpentPoints = 0;
foreach (var (skill, level) in comp.Skills)
{
var defaultLevel = defaultSkills?.GetValueOrDefault(skill, 1) ?? 1;
if (level > defaultLevel)
{
comp.SpentPoints += GetSkillTotalCost(skill, level) - GetSkillTotalCost(skill, defaultLevel);
}
}
}
/// <summary>
/// Recalculates spent points using job defaults
/// </summary>
public void RecalculateSpentPoints(EntityUid uid, string? jobId, SkillsComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
var defaultSkills = GetDefaultSkillsForJob(jobId);
RecalculateSpentPoints(uid, comp, ConvertToSkillTypeDict(defaultSkills));
}
/// <summary>
/// Converts byte-based dictionary to SkillType-based dictionary
/// </summary>
public Dictionary<SkillType, int> ConvertToSkillTypeDict(Dictionary<byte, int> byteDict)
{
var result = new Dictionary<SkillType, int>();
foreach (var (skillKey, level) in byteDict)
{
if (Enum.IsDefined(typeof(SkillType), (SkillType)skillKey))
{
result[(SkillType)skillKey] = level;
}
}
return result;
}
/// <summary>
/// Calculates the cost of increasing a skill to the specified level
/// </summary>
public int GetSkillTotalCost(SkillType skill, int targetLevel)
{
var costs = GetSkillCost(skill);
targetLevel = Math.Clamp(targetLevel, 1, 4);
int totalCost = 0;
for (int i = 0; i < targetLevel; i++)
{
totalCost += costs[i];
}
return totalCost;
}
/// <summary>
/// Calculates the cost of the current skill level
/// </summary>
public int GetCurrentSkillCost(SkillType skill, int currentLevel)
{
return GetSkillTotalCost(skill, currentLevel);
}
/// <summary>
/// Calculates the cost of increasing a skill by one level
/// </summary>
public int GetSkillUpgradeCost(SkillType skill, int currentLevel)
{
var costs = GetSkillCost(skill);
if (currentLevel >= 4) return 0;
return costs[currentLevel];
}
#endregion
#region Skill Application Methods
/// <summary>
/// Applies skills from the profile, taking into account the point limit
/// </summary>
private void ApplyProfileSkillsWithLimit(EntityUid uid, SkillsComponent skillsComp,
Dictionary<byte, int> profileSkills, Dictionary<SkillType, int> defaultSkills, int totalPoints)
{
var spentPoints = 0;
var tempSkills = new Dictionary<SkillType, int>(skillsComp.Skills);
foreach (var (skillKey, level) in profileSkills)
{
var skillType = (SkillType)skillKey;
if (!Enum.IsDefined(typeof(SkillType), skillType))
continue;
var clampedLevel = Math.Clamp(level, 1, 4);
var defaultLevel = defaultSkills.GetValueOrDefault(skillType, 1);
if (clampedLevel > defaultLevel)
{
var cost = GetSkillTotalCost(skillType, clampedLevel) - GetSkillTotalCost(skillType, defaultLevel);
if (spentPoints + cost <= totalPoints)
{
tempSkills[skillType] = clampedLevel;
spentPoints += cost;
}
else
{
var maxAffordableLevel = FindMaxAffordableLevel(skillType, defaultLevel, totalPoints - spentPoints);
if (maxAffordableLevel > defaultLevel)
{
tempSkills[skillType] = maxAffordableLevel;
spentPoints += GetSkillTotalCost(skillType, maxAffordableLevel) - GetSkillTotalCost(skillType, defaultLevel);
}
else
{
tempSkills[skillType] = defaultLevel;
}
}
}
else
{
tempSkills[skillType] = clampedLevel;
}
}
skillsComp.Skills = tempSkills;
}
/// <summary>
/// Finds the maximum available skill level within the budget
/// </summary>
private int FindMaxAffordableLevel(SkillType skill, int currentLevel, int availablePoints)
{
var maxLevel = currentLevel;
var currentCost = 0;
for (int targetLevel = currentLevel + 1; targetLevel <= 4; targetLevel++)
{
var additionalCost = GetSkillUpgradeCost(skill, targetLevel - 1);
if (currentCost + additionalCost <= availablePoints)
{
currentCost += additionalCost;
maxLevel = targetLevel;
}
else
{
break;
}
}
return maxLevel;
}
/// <summary>
/// Sets skill level with automatic detection of increase/decrease and default level check
/// </summary>
public bool TrySetSkillLevel(EntityUid uid, SkillType skill, int targetLevel, string? jobId = null, SkillsComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false;
targetLevel = Math.Clamp(targetLevel, 1, 4);
var currentLevel = comp.Skills.GetValueOrDefault(skill, 1);
if (currentLevel == targetLevel)
return true;
var defaultLevel = GetDefaultSkillLevelForJob(skill, jobId);
if (targetLevel < defaultLevel)
return false;
if (targetLevel > currentLevel)
{
return TryIncreaseToLevel(uid, skill, targetLevel, comp, defaultLevel, jobId);
}
else
{
return TryDecreaseToLevel(uid, skill, targetLevel, comp, defaultLevel, jobId);
}
}
/// <summary>
/// Increases skill to specified level
/// </summary>
private bool TryIncreaseToLevel(EntityUid uid, SkillType skill, int targetLevel,
SkillsComponent comp, int defaultLevel, string? jobId = null)
{
var currentLevel = comp.Skills.GetValueOrDefault(skill, 1);
var totalCost = 0;
for (int level = Math.Max(currentLevel, defaultLevel); level < targetLevel; level++)
{
totalCost += GetSkillUpgradeCost(skill, level);
}
if (comp.UnspentPoints < totalCost)
return false;
comp.UnspentPoints -= totalCost;
comp.Skills[skill] = targetLevel;
RecalculateSpentPoints(uid, jobId, comp);
Dirty(uid, comp);
return true;
}
/// <summary>
/// Decreases skill to specified level
/// </summary>
private bool TryDecreaseToLevel(EntityUid uid, SkillType skill, int targetLevel,
SkillsComponent comp, int defaultLevel, string? jobId = null)
{
var currentLevel = comp.Skills.GetValueOrDefault(skill, 1);
var totalRefund = 0;
for (int level = targetLevel; level < Math.Min(currentLevel, 4); level++)
{
if (level >= defaultLevel)
{
totalRefund += GetSkillUpgradeCost(skill, level);
}
}
comp.UnspentPoints += totalRefund;
comp.Skills[skill] = targetLevel;
RecalculateSpentPoints(uid, jobId, comp);
Dirty(uid, comp);
return true;
}
#endregion
#region Default Skills Methods
/// <summary>
/// Gets default skill level for job
/// </summary>
public int GetDefaultSkillLevelForJob(SkillType skill, string? jobId)
{
if (jobId != null && _prototype.TryIndex<JobPrototype>(jobId, out var jobPrototype))
{
return jobPrototype.DefaultSkills.GetValueOrDefault(skill, 1);
}
return 1; // Default base level
}
/// <summary>
/// Gets default skills for job
/// </summary>
public Dictionary<byte, int> GetDefaultSkillsForJob(string? jobId)
{
var defaultSkills = new Dictionary<byte, int>();
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
defaultSkills[(byte)skill] = 1;
}
if (jobId != null && _prototype.TryIndex<JobPrototype>(jobId, out var jobPrototype))
{
foreach (var (skill, level) in jobPrototype.DefaultSkills)
{
defaultSkills[(byte)skill] = level;
}
}
return defaultSkills;
}
/// <summary>
/// Checks if forced skills selection should be shown - only if ONLY default skills are set
/// </summary>
public bool ShouldForceSkillsSelection(EntityUid uid, string? jobId = null, SkillsComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false;
var defaultSkills = GetDefaultSkillsForJob(jobId);
foreach (var (skill, level) in comp.Skills)
{
var defaultLevel = defaultSkills.GetValueOrDefault((byte)skill, 1);
if (level != defaultLevel)
return false;
}
return true;
}
#endregion
#region Admin Methods
/// <summary>
/// Directly sets bonus points for admin control
/// </summary>
public void SetBonusPoints(EntityUid uid, int points, SkillsComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.BonusPoints = points;
Dirty(uid, comp);
}
/// <summary>
/// Directly sets skill level without point restrictions for admin control
/// </summary>
public void SetSkillLevelAdmin(EntityUid uid, SkillType skill, int level,
Dictionary<SkillType, int>? defaultSkills, SkillsComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
level = Math.Clamp(level, 1, 4);
comp.Skills[skill] = level;
RecalculateSpentPoints(uid, comp, defaultSkills);
Dirty(uid, comp);
}
/// <summary>
/// Resets all skills to level 1 for admin control
/// </summary>
public void ResetAllSkills(EntityUid uid, SkillsComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
comp.Skills[skill] = 1;
}
comp.UnspentPoints = 0;
comp.SpentPoints = 0;
comp.BonusPoints = 0;
Dirty(uid, comp);
}
#endregion
#region Utility Methods
/// <summary>
/// Gets total points for entity
/// </summary>
public int GetTotalPoints(EntityUid uid, string? jobId = null,
SkillsComponent? comp = null, HumanoidAppearanceComponent? humanoid = null)
{
if (!Resolve(uid, ref comp) || !Resolve(uid, ref humanoid))
return 0;
int bonusPoints = 0;
if (jobId != null && _prototype.TryIndex<JobPrototype>(jobId, out var jobPrototype))
bonusPoints = jobPrototype.BonusSkillPoints;
var racialBonus = CalculateRacialBonus(humanoid.Species, humanoid.Age);
return bonusPoints + racialBonus + comp.BonusPoints;
}
/// <summary>
/// Calculates racial bonus of skill points
/// </summary>
private int CalculateRacialBonus(string species, int age)
{
var bonus = 0;
foreach (var racialBonusProto in _prototype.EnumeratePrototypes<RacialSkillBonusPrototype>())
{
if (racialBonusProto.Species != species)
continue;
bonus = racialBonusProto.GetBonusForAge(age);
break;
}
return bonus;
}
#endregion
#region Initial Skills
private void OnMapInit(EntityUid uid, InitialSkillsComponent component, MapInitEvent args)
{
if (!_cfg.GetCVar(WLCVars.SkillsEnabled))
return;
var skillsComp = EnsureComp<SkillsComponent>(uid);
if (component.RandomizeAllSkills)
{
RandomizeAllSkills(uid, skillsComp, component.RandomMinLevel, component.RandomMaxLevel);
}
else
{
ApplyInitialSkills(uid, skillsComp, component);
}
RecalculateSpentPoints(uid, skillsComp);
RemCompDeferred<InitialSkillsComponent>(uid);
Dirty(uid, skillsComp);
}
/// <summary>
/// Applies the initial skills from the component
/// </summary>
public void ApplyInitialSkills(EntityUid uid, SkillsComponent skillsComp, InitialSkillsComponent initial)
{
if (initial.OverrideExisting)
{
skillsComp.Skills.Clear();
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
skillsComp.Skills[skill] = 1;
}
}
foreach (var (skill, level) in initial.InitialSkills)
{
skillsComp.Skills[skill] = Math.Clamp(level, 1, 4);
}
foreach (var skill in initial.RandomSkills)
{
var randomLevel = _random.Next(initial.RandomMinLevel, initial.RandomMaxLevel + 1);
skillsComp.Skills[skill] = Math.Clamp(randomLevel, 1, 4);
}
foreach (var (skill, addLevel) in initial.AddSkills)
{
var currentLevel = skillsComp.Skills.GetValueOrDefault(skill, 1);
skillsComp.Skills[skill] = Math.Clamp(currentLevel + addLevel, 1, 4);
}
}
/// <summary>
/// Randomizes all entity skills within the specified range
/// </summary>
public void RandomizeAllSkills(EntityUid uid, SkillsComponent? comp = null, int minLevel = 1, int maxLevel = 4)
{
if (!Resolve(uid, ref comp))
return;
minLevel = Math.Clamp(minLevel, 1, 4);
maxLevel = Math.Clamp(maxLevel, 1, 4);
foreach (SkillType skill in Enum.GetValues(typeof(SkillType)))
{
comp.Skills[skill] = _random.Next(minLevel, maxLevel + 1);
}
}
/// <summary>
/// Copies all skills and points from one entity to another
/// </summary>
public void CopySkills(EntityUid fromEntity, EntityUid toEntity, SkillsComponent? fromSkills = null)
{
if (!Resolve(fromEntity, ref fromSkills, false))
return;
var toSkills = EnsureComp<SkillsComponent>(toEntity);
toSkills.Skills.Clear();
foreach (var (skill, level) in fromSkills.Skills)
{
toSkills.Skills[skill] = level;
}
toSkills.UnspentPoints = fromSkills.UnspentPoints;
toSkills.SpentPoints = fromSkills.SpentPoints;
Dirty(toEntity, toSkills);
Log.Debug($"Copied skills from {ToPrettyString(fromEntity)} to {ToPrettyString(toEntity)}");
}
#endregion
}

View File

@@ -0,0 +1,57 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared._WL.Skills;
public abstract partial class SharedSkillsSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
InitializeSkills();
}
/// <summary>
/// Gets a skill prototype.
/// </summary>
public SkillPrototype GetSkillPrototype(SkillType skill)
{
var skillId = GetSkillPrototypeId(skill);
return _prototype.Index<SkillPrototype>(skillId);
}
/// <summary>
/// Gets the prototype ID for SkillType.
/// </summary>
public string GetSkillPrototypeId(SkillType skill)
{
foreach (var skillProto in _prototype.EnumeratePrototypes<SkillPrototype>())
{
if (skillProto.SkillType == skill)
return skillProto.ID;
}
throw new ArgumentException($"No skill prototype found for SkillType: {skill}");
}
/// <summary>
/// Gets an array of costs for a skill.
/// </summary>
public int[] GetSkillCost(SkillType skill)
{
var prototype = GetSkillPrototype(skill);
return prototype.Costs;
}
/// <summary>
/// Gets a color for the skill.
/// </summary>
public Color GetSkillColor(SkillType skill)
{
var prototype = GetSkillPrototype(skill);
return prototype.Color;
}
}

View File

@@ -0,0 +1,25 @@
namespace Content.Shared._WL.Skills;
public enum SkillType : byte
{
Bureaucracy,
Eva,
Piloting,
MechPiloting,
IT,
Atmospherics,
Construction,
Electrical,
Generators,
Anatomy,
Chemistry,
Medicine,
ComplexDevices,
Science,
Forensics,
Combat,
Weapons,
Botany,
Cooking,
Mixology
}

View File

@@ -0,0 +1,23 @@
using Robust.Shared.Serialization;
namespace Content.Shared._WL.Skills;
[ByRefEvent]
public record struct SkillsAddedEvent();
[Serializable, NetSerializable]
public sealed class SelectSkillPressedEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public SkillType Skill { get; }
public int TargetLevel { get; }
public string? JobId { get; }
public SelectSkillPressedEvent(NetEntity uid, SkillType skill, int targetLevel, string? jobId = null)
{
Uid = uid;
Skill = skill;
TargetLevel = targetLevel;
JobId = jobId;
}
}

View File

@@ -0,0 +1,102 @@
using Content.Shared.Eui;
using Robust.Shared.Serialization;
namespace Content.Shared._WL.Skills.UI;
[Serializable, NetSerializable]
public sealed class SkillsEuiState : EuiStateBase
{
public readonly string JobId;
public readonly Dictionary<byte, int> CurrentSkills;
public readonly Dictionary<byte, int> DefaultSkills;
public readonly int TotalPoints;
public readonly int SpentPoints;
public SkillsEuiState(string jobId, Dictionary<byte, int> currentSkills,
Dictionary<byte, int> defaultSkills, int totalPoints, int spentPoints)
{
JobId = jobId;
CurrentSkills = currentSkills;
DefaultSkills = defaultSkills;
TotalPoints = totalPoints;
SpentPoints = spentPoints;
}
}
[Serializable, NetSerializable]
public sealed class SkillsEuiClosedMessage : EuiMessageBase
{
}
[Serializable, NetSerializable]
public sealed class SkillsEuiSkillChangedMessage : EuiMessageBase
{
public readonly string JobId;
public readonly byte SkillKey;
public readonly int NewLevel;
public SkillsEuiSkillChangedMessage(string jobId, byte skillKey, int newLevel)
{
JobId = jobId;
SkillKey = skillKey;
NewLevel = newLevel;
}
}
#region Admin
[Serializable, NetSerializable]
public sealed class SkillsAdminEuiState : EuiStateBase
{
public readonly bool HasSkills;
public readonly Dictionary<byte, int> CurrentSkills;
public readonly int SpentPoints;
public readonly int BonusPoints;
public readonly string CurrentJob;
public readonly string EntityName;
public SkillsAdminEuiState(bool hasSkills, Dictionary<byte, int> currentSkills,
int spentPoints, int bonusPoints, string currentJob, string entityName)
{
HasSkills = hasSkills;
CurrentSkills = currentSkills;
SpentPoints = spentPoints;
BonusPoints = bonusPoints;
CurrentJob = currentJob;
EntityName = entityName;
}
}
[Serializable, NetSerializable]
public sealed class SkillsAdminEuiClosedMessage : EuiMessageBase
{
}
[Serializable, NetSerializable]
public sealed class SkillsAdminEuiResetMessage : EuiMessageBase
{
}
[Serializable, NetSerializable]
public sealed class SkillsAdminEuiSkillChangedMessage : EuiMessageBase
{
public readonly byte SkillKey;
public readonly int NewLevel;
public SkillsAdminEuiSkillChangedMessage(byte skillKey, int newLevel)
{
SkillKey = skillKey;
NewLevel = newLevel;
}
}
[Serializable, NetSerializable]
public sealed class SkillsAdminEuiPointsChangedMessage : EuiMessageBase
{
public readonly int NewBonusPoints;
public SkillsAdminEuiPointsChangedMessage(int newBonusPoints)
{
NewBonusPoints = newBonusPoints;
}
}
#endregion

View File

@@ -0,0 +1,81 @@
# UI
skills-admin-window-title = Управление навыками
skills-admin-target = Цель:
skills-admin-job = Должность:
skills-admin-points-control = Управление очками
skills-admin-bonus-points = Бонусные очки:
skills-admin-spent-points = Потраченные очки:
skills-admin-apply-points = Применить очки
skills-admin-close = Закрыть
skills-admin-reset-all = Сбросить всё
skills-admin-skills-list = Управление навыками
skills-admin-skills-no-job = <НЕТУ ДОЛЖНОСТИ>
skills-admin-verb-manage = Управление навыками
skill-window = Очки
skill-window-tooltip = Распределение навыков для роли.
skill-level-1 = Новичок
skill-level-2 = Любитель
skill-level-3 = Специалист
skill-level-4 = Профессионал
skill-available-points = Доступное количество очков:
skill-total-cost = Потрачено очков: {$cost}
skill-locked-default = Заблокирован
skill-free-default = Бесплатно (дефолтный уровень)
skill-free-upgraded = Бесплатно
skills-forced-window-title = Настройка навыков
skills-forced-warning = Вы должны распределить все доступные очки навыков перед началом игры
skills-confirm-button = Подтвердить
skills-unspent-warning = У вас остались неиспользованные очки! Потратьте их все для продолжения.
character-info-skills-button = Навыки
skill-bureaucracy = Бюрократия
skill-eva = Внекорабельная деятельность
skill-piloting = Пилотирование
skill-mechpiloting = Управление мехами
skill-it = Информационные технологии
skill-atmospherics = Атмосферика
skill-construction = Строительство
skill-electrical = Электротехника
skill-generators = Генераторы
skill-anatomy = Анатомия
skill-chemistry = Химия
skill-medicine = Медицина
skill-complexdevices = Сложные устройства
skill-science = Наука
skill-forensics = Криминалистика
skill-combat = Ближний бой
skill-weapons = Обращение с оружием
skill-botany = Ботаника
skill-cooking = Готовка
skill-mixology = Миксология
skill-bureaucracy-desc = Умение работать с документами и знание корпоративных регуляций.
skill-eva-desc = Знания и опыт работы со скафандрами и в космосе. Влияет на эффективность внекорабельной деятельности.
skill-piloting-desc = Знание устройства космических кораблей и умение их пилотировать. Определяет способности в управлении шаттлами.
skill-mechpiloting-desc = Умение управлять мехами и экзокостюмами. Требуется для эффективного использования роботизированных костюмов.
skill-it-desc = Знания в сферах телекоммуникаций, связи, программирования и работы с искусственным интеллектом.
skill-atmospherics-desc = Знания в области физики газов и работы с трубопроводами. Влияет на эффективность работы с атмосферными системами.
skill-construction-desc = Умения в сфере проектировки, строительства и работы с различными материалами. Определяет качество и скорость строительства.
skill-electrical-desc = Знание электроприборов и проводки. Влияет на умение ремонтировать и обслуживать электрические системы.
skill-generators-desc = Умение работать с различными генераторами энергии. Определяет эффективность обслуживания энергосистем.
skill-anatomy-desc = Знание работы и структуры тела и органов. Влияет на понимание биологических процессов и медицинские навыки.
skill-chemistry-desc = Опыт работы с химическим оборудованием, химикатами и их взаимодействиями. Не включает знание медицины.
skill-medicine-desc = Умение лечить людей и ксенорасы, работа с медицинским оборудованием. Определяет эффективность медицинской помощи.
skill-complexdevices-desc = Умение собирать сложные устройства, работать с высокотехнологическим оборудованием и обслуживать робототехнику.
skill-science-desc = Опыт работы в области науки, разработок и умение применять научные методы. Влияет на исследовательские способности.
skill-forensics-desc = Умение раскрывать преступления: работа с уликами, вещественными доказательствами, осмотр места преступления.
skill-combat-desc = Знание боевых приёмов и умение вести рукопашный бой, точно бросать различные предметы.
skill-weapons-desc = Опыт в стрельбе и ведении боя. Определяет, каким оружием может пользоваться персонаж.
skill-botany-desc = Умение ухаживать за растениями и знания в сфере ботанической науки, селекции и мутаций.
skill-cooking-desc = Мастерство в приготовлении различных блюд и пользовании кухонным оборудованием.
skill-mixology-desc = Умение в разливе, смешивании напитков и пользовании барным оборудованием.
# System
skills-admin-notify-skills-changed = Ваши навыки были изменены! Откройте меню навыков для просмотра изменений.
skills-admin-notify-points-added = Вам были начислены дополнительный очки навыков!
skills-admin-notify-points-removed = С вас были списаны очки навыков!
skills-admin-notify-skills-reset = Ваши навыки были полностью сброшены!

View File

@@ -30,6 +30,12 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 3
Forensics: 2
# WL-Skills-end
- type: startingGear
id: IAAGear

View File

@@ -26,6 +26,18 @@
- Engineering
- External
- Atmospherics
# WL-Skills-start
bonusSkillPoints: 4
defaultSkills:
Bureaucracy: 2
Eva: 3
IT: 2
Atmospherics: 3
Construction: 3
Electrical: 3
Generators: 3
ComplexDevices: 2
# WL-Skills-end
- type: startingGear
id: SeniorEngineerGear

View File

@@ -25,6 +25,14 @@
- Medical
- Maintenance
- Chemistry
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 2
Eva: 3
Anatomy: 3
Medicine: 3
# WL-Skills-end
- type: startingGear
id: SeniorPhysicianGear

View File

@@ -18,6 +18,15 @@
access:
- Research
- Maintenance
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
IT: 2
Chemistry: 2
ComplexDevices: 2
Science: 3
# WL-Skills-end
- type: startingGear
id: SeniorResearcherGear

View File

@@ -28,6 +28,16 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 4
defaultSkills:
Bureaucracy: 2
Eva: 3
Anatomy: 2
Medicine: 3
Combat: 2
Weapons: 2
# WL-Skills-end
- type: startingGear
id: BrigmedicGear

View File

@@ -29,6 +29,16 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 2
Eva: 3
Piloting: 3
Forensics: 2
Combat: 3
Weapons: 3
# WL-Skills-end
- type: startingGear
id: PilotGear

View File

@@ -34,6 +34,16 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
Eva: 3
Piloting: 2
Forensics: 2
Combat: 3
Weapons: 3
# WL-Skills-end
- type: startingGear
id: SeniorOfficerGear

View File

@@ -892,6 +892,11 @@
attributes:
proper: true
gender: male
# WL-Skills-start
- type: InitialSkills
initialSkills:
Mixology: 3
# WL-Skills-end
- type: entity
name: Tropico

View File

@@ -31,6 +31,7 @@
tags:
- DoorBumpOpener
- StunImmune
- type: InitialSkills # WL-Skills
- type: entity
abstract: true

View File

@@ -68,7 +68,29 @@
- NamesMilitaryFirstLeader
- NamesMilitaryLast
nameFormat: name-format-ert
# WL-Skills-start
- type: InitialSkills
randomMaxLevel: 3
initialSkills:
Bureaucracy: 4
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 4
Atmospherics: 4
Construction: 4
Electrical: 4
Generators: 4
Anatomy: 4
Chemistry: 4
Medicine: 4
ComplexDevices: 4
Science: 4
Forensics: 4
Combat: 4
Weapons: 4
randomSkills: [Botany, Cooking, Mixology]
# WL-Skills-end
## ERT Leader
@@ -107,6 +129,29 @@
- NamesMilitaryFirstLeader
- NamesMilitaryLast
nameFormat: name-format-ert
# WL-Skills-start
- type: InitialSkills
randomMaxLevel: 3
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 3
Atmospherics: 2
Construction: 2
Electrical: 2
Generators: 2
Anatomy: 2
Chemistry: 2
Medicine: 3
ComplexDevices: 2
Science: 2
Forensics: 3
Combat: 4
Weapons: 4
randomSkills: [Botany, Cooking, Mixology]
# WL-Skills-end
- type: entity
id: RandomHumanoidSpawnerERTLeaderEVA
@@ -264,6 +309,29 @@
- type: Loadout
prototypes: [ ERTJanitorGear ]
roleLoadout: [ RoleSurvivalExtended ]
# WL-Skills-start
- type: InitialSkills
randomMaxLevel: 3
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 3
MechPiloting: 3
IT: 2
Atmospherics: 2
Construction: 2
Electrical: 2
Generators: 2
Anatomy: 2
Chemistry: 3
Medicine: 3
ComplexDevices: 2
Science: 2
Forensics: 3
Combat: 4
Weapons: 4
randomSkills: [Botany, Cooking, Mixology]
# WL-Skills-end
- type: entity
id: RandomHumanoidSpawnerERTJanitorEVA
@@ -330,6 +398,29 @@
- type: Loadout
prototypes: [ ERTEngineerGear ]
roleLoadout: [ RoleSurvivalExtended ]
# WL-Skills-start
- type: InitialSkills
randomMaxLevel: 3
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 4
Atmospherics: 4
Construction: 4
Electrical: 4
Generators: 4
Anatomy: 2
Chemistry: 2
Medicine: 3
ComplexDevices: 4
Science: 2
Forensics: 3
Combat: 4
Weapons: 4
randomSkills: [Botany, Cooking, Mixology]
# WL-Skills-end
- type: entity
id: RandomHumanoidSpawnerERTEngineerEVA
@@ -398,6 +489,29 @@
- type: Loadout
prototypes: [ ERTSecurityGear ]
roleLoadout: [ RoleSurvivalExtended ]
# WL-Skills-start
- type: InitialSkills
randomMaxLevel: 3
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 3
MechPiloting: 3
IT: 3
Atmospherics: 2
Construction: 2
Electrical: 2
Generators: 2
Anatomy: 2
Chemistry: 2
Medicine: 3
ComplexDevices: 2
Science: 2
Forensics: 3
Combat: 4
Weapons: 4
randomSkills: [Botany, Cooking, Mixology]
# WL-Skills-end
- type: entity
id: RandomHumanoidSpawnerERTSecurityEVA
@@ -487,6 +601,29 @@
- type: Loadout
prototypes: [ ERTMedicalGear ]
roleLoadout: [ RoleSurvivalExtended ]
# WL-Skills-start
- type: InitialSkills
randomMaxLevel: 3
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 3
MechPiloting: 3
IT: 2
Atmospherics: 2
Construction: 2
Electrical: 2
Generators: 2
Anatomy: 4
Chemistry: 4
Medicine: 4
ComplexDevices: 2
Science: 2
Forensics: 3
Combat: 4
Weapons: 4
randomSkills: [Botany, Cooking, Mixology]
# WL-Skills-end
- type: entity
id: RandomHumanoidSpawnerERTMedicalEVA

View File

@@ -103,6 +103,8 @@
normalState: human_small_fire # Corvax WL /tg/ resprite
alternateState: human_big_fire # Corvax WL /tg/ resprite
- type: FlashImmunity
- type: InitialSkills # WL-Skills
randomizeAllSkills: true # WL-Skills
- type: Inventory
femaleDisplacements:
jumpsuit:

View File

@@ -267,6 +267,31 @@
- NamesNinjaTitle
- NamesNinja
nameFormat: name-format-ninja
# WL-Skills-start
- type: InitialSkills
initialSkills:
Eva: 3
MechPiloting: 3
Combat: 4
randomSkills:
- Bureaucracy
- Piloting
- IT
- Atmospherics
- Construction
- Electrical
- Generators
- Anatomy
- Chemistry
- Medicine
- ComplexDevices
- Science
- Forensics
- Weapons
- Botany
- Cooking
- Mixology
# WL-Skills-end
mindRoles:
- MindRoleNinja
- type: DynamicRuleCost

View File

@@ -114,7 +114,7 @@
id: Nukeops
components:
- type: GameRule
minPlayers: 20
minPlayers: 0
- type: LoadMapRule
mapPath: /Maps/Nonstations/nukieplanet.yml
- type: AntagSelection
@@ -135,6 +135,26 @@
- type: NpcFactionMember
factions:
- Syndicate
# WL-Skills-start
- type: InitialSkills
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 3
Atmospherics: 3
Construction: 4
Electrical: 4
Generators: 3
Anatomy: 3
Chemistry: 3
Medicine: 3
ComplexDevices: 4
Science: 2
Combat: 4
Weapons: 4
# WL-Skills-end
mindRoles:
- MindRoleNukeopsCommander
- prefRoles: [ NukeopsMedic ]
@@ -152,6 +172,26 @@
- type: NpcFactionMember
factions:
- Syndicate
# WL-Skills-start
- type: InitialSkills
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 3
Atmospherics: 3
Construction: 4
Electrical: 4
Generators: 3
Anatomy: 3
Chemistry: 3
Medicine: 3
ComplexDevices: 4
Science: 2
Combat: 4
Weapons: 4
# WL-Skills-end
mindRoles:
- MindRoleNukeopsMedic
- prefRoles: [ Nukeops ]
@@ -171,6 +211,26 @@
- type: NpcFactionMember
factions:
- Syndicate
# WL-Skills-start
- type: InitialSkills
initialSkills:
Bureaucracy: 3
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 3
Atmospherics: 3
Construction: 4
Electrical: 4
Generators: 3
Anatomy: 3
Chemistry: 3
Medicine: 3
ComplexDevices: 4
Science: 2
Combat: 4
Weapons: 4
# WL-Skills-end
mindRoles:
- MindRoleNukeops
- type: DynamicRuleCost
@@ -198,7 +258,7 @@
id: Traitor
components:
- type: GameRule
minPlayers: 5
minPlayers: 0
delay:
min: 240
max: 420
@@ -214,6 +274,33 @@
lateJoinAdditional: false
mindRoles:
- MindRoleTraitor
# WL-Skills-start
components:
- type: InitialSkills
overrideExisting: false
initialSkills:
Eva: 3
MechPiloting: 3
addSkills:
Bureaucracy: 1
Piloting: 1
IT: 1
Atmospherics: 1
Construction: 1
Electrical: 1
Generators: 1
Anatomy: 1
Chemistry: 1
Medicine: 1
ComplexDevices: 1
Science: 1
Forensics: 1
Combat: 1
Weapons: 1
Botany: 1
Cooking: 1
Mixology: 1
# WL-Skills-end
- type: entity
id: TraitorReinforcement
@@ -228,6 +315,26 @@
- prefRoles: [ Traitor ]
mindRoles:
- MindRoleTraitorReinforcement
# WL-Skills-start
components:
- type: InitialSkills
initialSkills:
Eva: 3
Piloting: 3
MechPiloting: 3
IT: 3
Atmospherics: 3
Construction: 3
Electrical: 3
Anatomy: 3
Chemistry: 3
Medicine: 3
ComplexDevices: 3
Science: 3
Combat: 4
Weapons: 4
randomSkills: [Bureaucracy, Generators, Forensics, Botany, Cooking, Mixology]
# WL-Skills-end
- type: entity
id: Changeling
@@ -280,6 +387,32 @@
components:
- type: Revolutionary
- type: HeadRevolutionary
# WL-Skills-start
- type: InitialSkills
overrideExisting: false
initialSkills:
Eva: 3
MechPiloting: 3
addSkills:
Bureaucracy: 1
Piloting: 1
IT: 1
Atmospherics: 1
Construction: 1
Electrical: 1
Generators: 1
Anatomy: 1
Chemistry: 1
Medicine: 1
ComplexDevices: 1
Science: 1
Forensics: 1
Combat: 1
Weapons: 1
Botany: 1
Cooking: 1
Mixology: 1
# WL-Skills-end
mindRoles:
- MindRoleHeadRevolutionary
- type: DynamicRuleCost

View File

@@ -31,6 +31,32 @@
stealthy: true
# components: # Corvax-MRP
# - type: Pacified
# WL-Skills-start
- type: InitialSkills
overrideExisting: false
initialSkills:
Eva: 3
MechPiloting: 3
addSkills:
Bureaucracy: 1
Piloting: 1
IT: 1
Atmospherics: 1
Construction: 1
Electrical: 1
Generators: 1
Anatomy: 1
Chemistry: 1
Medicine: 1
ComplexDevices: 1
Science: 1
Forensics: 1
Combat: 1
Weapons: 1
Botany: 1
Cooking: 1
Mixology: 1
# WL-Skills-end
mindRoles:
- MindRoleThief
briefing:

View File

@@ -24,6 +24,12 @@
- !type:GiveItemOnHolidaySpecial
holiday: BoxingDay
prototype: BoxCardboard
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
Piloting: 2
# WL-Skills-end
- type: startingGear
id: CargoTechGear

View File

@@ -43,6 +43,13 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 3
Eva: 3
Piloting: 2
# WL-Skills-end
- type: startingGear
id: QuartermasterGear

View File

@@ -24,6 +24,16 @@
- Salvage
- Maintenance
- External
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Eva: 3
Piloting: 2
Construction: 2
Medicine: 2
Science: 2
Combat: 2
# WL-Skills-end
- type: startingGear
id: SalvageSpecialistGear

View File

@@ -18,6 +18,7 @@
supervisors: job-supervisors-everyone
# access: # WL-Changes
# - Maintenance
bonusSkillPoints: 16 # WL-Skills / Why?
- type: startingGear
id: PassengerGear

View File

@@ -19,6 +19,14 @@
extendedAccess:
- Kitchen
- Hydroponics
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Chemistry: 2
Weapons: 2
Botany: 2
Mixology: 3
# WL-Skills-end
- type: startingGear
id: BartenderGear

View File

@@ -23,6 +23,13 @@
- !type:GiveItemOnHolidaySpecial
holiday: FourTwenty
prototype: CannabisSeeds
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Chemistry: 2
Science: 2
Botany: 3
# WL-Skills-end
- type: startingGear
id: BotanistGear

View File

@@ -27,6 +27,11 @@
- !type:AgeRequirement
minAge: 18
# WL-Changes-AgeRequirement End
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
# WL-Skills-end
- type: startingGear
id: ChaplainGear

View File

@@ -24,6 +24,13 @@
extendedAccess:
- Hydroponics
- Bar
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Anatomy: 2
Botany: 2
Cooking: 3
# WL-Skills-end
- type: startingGear
id: ChefGear

View File

@@ -34,6 +34,7 @@
interval: 10
- !type:AddImplantSpecial
implants: [ SadTromboneImplant ]
bonusSkillPoints: 10 # WL-Skills
- type: startingGear
id: ClownGear

View File

@@ -17,6 +17,7 @@
- !type:GiveItemOnHolidaySpecial
holiday: GarbageDay
prototype: WeaponRevolverInspector
bonusSkillPoints: 6 # WL-Skills
- type: startingGear
id: JanitorGear

View File

@@ -15,6 +15,11 @@
access:
- Service
- Maintenance
# WL-Skills-start
bonusSkillPoints: 10
defaultSkills:
Bureaucracy: 2
# WL-Skills-end
- type: startingGear
id: LibrarianGear

View File

@@ -20,6 +20,7 @@
- type: MimePowers
preventWriting: true
- type: FrenchAccent
bonusSkillPoints: 10 # WL-Skills
- type: startingGear
id: MimeGear

View File

@@ -22,6 +22,7 @@
- !type:GiveItemOnHolidaySpecial
holiday: MikuDay
prototype: BoxPerformer
bonusSkillPoints: 8 # WL-Skills
- type: startingGear
id: MusicianGear

View File

@@ -23,6 +23,14 @@
- Kitchen
extendedAccess:
- Hydroponics
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Chemistry: 2
Botany: 2
Cooking: 2
Mixology: 3
# WL-Skills-end
- type: startingGear
id: ServiceWorkerGear

View File

@@ -41,6 +41,14 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 3
Eva: 3
Piloting: 3
Weapons: 2
# WL-Skills-end
- type: startingGear
id: CaptainGear

View File

@@ -65,6 +65,12 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 3
IT: 2
# WL-Skills-end
- type: startingGear
id: HoPGear

View File

@@ -23,6 +23,16 @@
- !type:GiveItemOnHolidaySpecial
holiday: FirefighterDay
prototype: FireAxe
# WL-Skills-start
bonusSkillPoints: 4
defaultSkills:
Eva: 3
IT: 3
Atmospherics: 2
Construction: 3
Electrical: 3
Generators: 3
# WL-Skills-end
- type: startingGear
id: AtmosphericTechnicianGear

View File

@@ -41,6 +41,18 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 4
defaultSkills:
Bureaucracy: 3
Eva: 3
IT: 3
Atmospherics: 3
Construction: 3
Electrical: 3
Generators: 3
ComplexDevices: 2
# WL-Skills-end
- type: startingGear
id: ChiefEngineerGear

View File

@@ -24,6 +24,17 @@
- External
extendedAccess:
- Atmospherics
# WL-Skills-start
bonusSkillPoints: 4
defaultSkills:
Eva: 3
IT: 2
Atmospherics: 2
Construction: 3
Electrical: 3
Generators: 3
ComplexDevices: 2
# WL-Skills-end
- type: startingGear
id: StationEngineerGear

View File

@@ -21,6 +21,15 @@
- Maintenance
- Engineering
- External
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
IT: 2
Atmospherics: 2
Construction: 2
Electrical: 2
Generators: 2
# WL-Skills-end
- type: startingGear
id: TechnicalAssistantGear

View File

@@ -19,6 +19,12 @@
- Medical
- Chemistry
- Maintenance
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Chemistry: 3
Medicine: 2
# WL-Skills-end
- type: startingGear
id: ChemistGear

View File

@@ -42,6 +42,15 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 3
Eva: 3
Anatomy: 3
Chemistry: 2
Medicine: 3
# WL-Skills-end
- type: startingGear
id: CMOGear

View File

@@ -26,6 +26,14 @@
- !type:GiveItemOnHolidaySpecial
holiday: DoctorDay
prototype: WehMedipen
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 2
Eva: 2
Anatomy: 3
Medicine: 3
# WL-Skills-end
- type: startingGear
id: DoctorGear

View File

@@ -22,6 +22,12 @@
access:
- Medical
- Maintenance
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Anatomy: 2
Medicine: 2
# WL-Skills-end
- type: startingGear
id: MedicalInternGear

View File

@@ -17,6 +17,14 @@
- Maintenance
extendedAccess:
- Chemistry
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Eva: 3
Anatomy: 2
Medicine: 3
Combat: 2
# WL-Skills-end
- type: startingGear
id: ParamedicGear

View File

@@ -15,6 +15,29 @@
jobEntity: StationAiBrain
jobPreviewEntity: PlayerStationAiPreview
applyTraits: false
# WL-Skills-start
defaultSkills:
Bureaucracy: 4
Eva: 3
Piloting: 4
MechPiloting: 3
IT: 4
Atmospherics: 4
Construction: 4
Electrical: 4
Generators: 4
Anatomy: 4
Chemistry: 4
Medicine: 4
ComplexDevices: 4
Science: 4
Forensics: 4
Combat: 4
Weapons: 4
Botany: 4
Cooking: 4
Mixology: 4
# WL-Skills-end
- type: job
id: Borg

View File

@@ -22,6 +22,11 @@
access:
- Research
- Maintenance
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Science: 2
# WL-Skills-end
- type: startingGear
id: ResearchAssistantGear

View File

@@ -36,6 +36,14 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
IT: 2
ComplexDevices: 3
Science: 3
# WL-Skills-end
- type: startingGear
id: ResearchDirectorGear

View File

@@ -21,6 +21,13 @@
access:
- Research
- Maintenance
# WL-Skills-start
bonusSkillPoints: 10
defaultSkills:
Bureaucracy: 2
ComplexDevices: 2
Science: 3
# WL-Skills-end
- type: startingGear
id: ScientistGear

View File

@@ -34,6 +34,16 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
Eva: 3
IT: 2
Forensics: 3
Combat: 3
Weapons: 2
# WL-Skills-end
- type: startingGear
id: DetectiveGear

View File

@@ -43,6 +43,16 @@
- !type:AddComponentSpecial
components:
- type: CommandStaff
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 3
Eva: 3
Piloting: 2
Forensics: 2
Combat: 3
Weapons: 3
# WL-Skills-end
- type: startingGear
id: HoSGear

View File

@@ -30,6 +30,13 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 4
defaultSkills:
Bureaucracy: 2
Combat: 2
Weapons: 2
# WL-Skills-end
- type: startingGear
id: SecurityCadetGear

View File

@@ -32,6 +32,15 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 2
Eva: 3
Forensics: 2
Combat: 3
Weapons: 3
# WL-Skills-end
- type: startingGear
id: SecurityOfficerGear

View File

@@ -32,6 +32,15 @@
special:
- !type:AddImplantSpecial
implants: [ MindShieldImplant ]
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Bureaucracy: 3
Eva: 3
Forensics: 2
Combat: 3
Weapons: 3
# WL-Skills-end
- type: startingGear
id: WardenGear

View File

@@ -21,6 +21,11 @@
- !type:GiveItemOnHolidaySpecial
holiday: BoxingDay
prototype: ClothingHandsGlovesBoxingRigged
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Combat: 3
# WL-Skills-end
- type: startingGear
id: BoxerGear

View File

@@ -16,6 +16,13 @@
- Maintenance
extendedAccess:
- Chemistry
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
Chemistry: 2
Combat: 2
# WL-Skills-end
- type: startingGear
id: PsychologistGear

View File

@@ -15,6 +15,12 @@
access:
- Service
- Maintenance
# WL-Skills-start
bonusSkillPoints: 8
defaultSkills:
Bureaucracy: 2
IT: 2
# WL-Skills-end
- type: startingGear
id: ReporterGear

View File

@@ -18,6 +18,12 @@
- !type:GiveItemOnHolidaySpecial
holiday: MonkeyDay
prototype: MonkeyCubeBox
# WL-Skills-start
bonusSkillPoints: 6
defaultSkills:
Anatomy: 2
Medicine: 2
# WL-Skills-end
- type: startingGear
id: ZookeeperGear

View File

@@ -0,0 +1,169 @@
- type: racialSkillBonus
id: HumanSkillBonuses
species: Human
ageBonuses:
22: 2
24: 2
30: 1
40: 1
55: 1
70: 1
- type: racialSkillBonus
id: ReptilianSkillBonuses
species: Reptilian
ageBonuses:
22: 2
24: 2
30: 2
40: 2
55: 1
70: 1
- type: racialSkillBonus
id: DionaSkillBonuses
species: Diona
ageBonuses:
22: 1
40: 1
80: 2
150: 2
250: 2
400: 4
- type: racialSkillBonus
id: SlimePersonSkillBonuses
species: SlimePerson
ageBonuses:
22: 1
24: 1
30: 1
40: 1
55: 1
70: 1
- type: racialSkillBonus
id: FelinidSkillBonuses
species: Felinid
ageBonuses:
22: 2
24: 2
30: 2
40: 1
55: 1
- type: racialSkillBonus
id: HumanoidKidanesSkillBonuses
species: HumanoidKidanes
ageBonuses:
22: 4
24: 2
30: 2
40: 1
55: 1
- type: racialSkillBonus
id: VulpkaninSkillBonuses
species: Vulpkanin
ageBonuses:
22: 2
24: 2
30: 1
40: 1
55: 1
70: 1
- type: racialSkillBonus
id: TajaranSkillBonuses
species: Tajaran
ageBonuses:
22: 2
24: 1
30: 1
40: 1
55: 1
- type: racialSkillBonus
id: AndroidSkillBonuses
species: Android
ageBonuses:
22: 4
24: 2
30: 2
40: 2
55: 1
70: 1
- type: racialSkillBonus
id: MurineSkillBonuses
species: Murine
ageBonuses:
22: 4
24: 4
30: 2
40: 2
55: 2
- type: racialSkillBonus
id: MothSkillBonuses
species: Moth
ageBonuses:
22: 2
24: 2
30: 2
40: 1
55: 1
70: 1
- type: racialSkillBonus
id: CischiSkillBonuses
species: Cischi
ageBonuses:
22: 2
24: 2
30: 2
40: 1
55: 1
70: 1
- type: racialSkillBonus
id: ArachnidSkillBonuses
species: Arachnid
ageBonuses:
22: 2
24: 2
30: 1
40: 1
55: 1
70: 1
- type: racialSkillBonus
id: GolemSkillBonuses
species: Golem
ageBonuses:
22: 1
40: 1
80: 2
150: 2
250: 2
400: 4
- type: racialSkillBonus
id: AkulaSkillBonuses
species: Akula
ageBonuses:
22: 2
24: 2
30: 2
40: 2
55: 1
70: 1
- type: racialSkillBonus
id: IpcSkillBonuses
species: Ipc
ageBonuses:
1: 4
18: 2
30: 4
60: 2

View File

@@ -0,0 +1,119 @@
- type: skill
id: Bureaucracy
skillType: Bureaucracy
costs: [0, 1, 2, 2]
color: "#eed2d2"
- type: skill
id: Eva
skillType: Eva
costs: [0, 0, 4, 0]
color: "#c9daf8"
- type: skill
id: Piloting
skillType: Piloting
costs: [0, 2, 2, 4]
color: "#cad6e4"
- type: skill
id: MechPiloting
skillType: MechPiloting
costs: [0, 0, 2, 0]
color: "#e0c9e6"
- type: skill
id: IT
skillType: IT
costs: [0, 1, 2, 2]
color: "#d5a6bd"
- type: skill
id: Atmospherics
skillType: Atmospherics
costs: [0, 2, 2, 4]
color: "#b4e4d4"
- type: skill
id: Construction
skillType: Construction
costs: [0, 2, 2, 2]
color: "#f0d5c7"
- type: skill
id: Electrical
skillType: Electrical
costs: [0, 4, 2, 2]
color: "#fae9c1"
- type: skill
id: Generators
skillType: Generators
costs: [0, 4, 4, 4]
color: "#fae9c1"
- type: skill
id: Anatomy
skillType: Anatomy
costs: [0, 4, 4, 8]
color: "#b7dff0"
- type: skill
id: Chemistry
skillType: Chemistry
costs: [0, 4, 4, 8]
color: "#ffc7b0"
- type: skill
id: Medicine
skillType: Medicine
costs: [0, 2, 4, 8]
color: "#a5d4eb"
- type: skill
id: ComplexDevices
skillType: ComplexDevices
costs: [0, 2, 2, 4]
color: "#cab5dd"
- type: skill
id: Science
skillType: Science
costs: [0, 2, 2, 4]
color: "#eca8eb"
- type: skill
id: Forensics
skillType: Forensics
costs: [0, 1, 4, 2]
color: "#e78080"
- type: skill
id: Combat
skillType: Combat
costs: [0, 2, 2, 2]
color: "#ea9999"
- type: skill
id: Weapons
skillType: Weapons
costs: [0, 2, 4, 8]
color: "#caa1a1"
- type: skill
id: Botany
skillType: Botany
costs: [0, 1, 2, 2]
color: "#b1e7a0"
- type: skill
id: Cooking
skillType: Cooking
costs: [0, 1, 2, 2]
color: "#ebb59f"
- type: skill
id: Mixology
skillType: Mixology
costs: [0, 1, 2, 2]
color: "#dacbbe"