merge master and hope things don't break
This commit is contained in:
parent
a1480c1ffd
commit
2261ec8f5f
|
|
@ -44,7 +44,7 @@ namespace Content.Benchmarks
|
|||
for (var i = 0; i < Aabbs1.Length; i++)
|
||||
{
|
||||
var aabb = Aabbs1[i];
|
||||
_b2Tree.CreateProxy(aabb, i);
|
||||
_b2Tree.CreateProxy(aabb, uint.MaxValue, i);
|
||||
_tree.Add(i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
var newPlayerThreshold = 0;
|
||||
_cfg.OnValueChanged(CCVars.NewPlayerThreshold, (val) => { newPlayerThreshold = val; }, true);
|
||||
|
||||
var uiController = _ui.GetUIController<AHelpUIController>();
|
||||
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
|
||||
return;
|
||||
|
|
@ -59,9 +62,9 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
var sb = new StringBuilder();
|
||||
|
||||
if (info.Connected)
|
||||
sb.Append('●');
|
||||
sb.Append(info.ActiveThisRound ? '⚫' : '◐');
|
||||
else
|
||||
sb.Append(info.ActiveThisRound ? '○' : '·');
|
||||
sb.Append(info.ActiveThisRound ? '⭘' : '·');
|
||||
|
||||
sb.Append(' ');
|
||||
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
||||
|
|
@ -73,10 +76,12 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
sb.Append(' ');
|
||||
}
|
||||
|
||||
// Mark antagonists with symbol
|
||||
if (info.Antag && info.ActiveThisRound)
|
||||
sb.Append(new Rune(0x1F5E1)); // 🗡
|
||||
|
||||
if (info.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)))
|
||||
// Mark new players with symbol
|
||||
if (IsNewPlayer(info))
|
||||
sb.Append(new Rune(0x23F2)); // ⏲
|
||||
|
||||
sb.AppendFormat("\"{0}\"", text);
|
||||
|
|
@ -84,6 +89,19 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
return sb.ToString();
|
||||
};
|
||||
|
||||
// <summary>
|
||||
// Returns true if the player's overall playtime is under the set threshold
|
||||
// </summary>
|
||||
bool IsNewPlayer(PlayerInfo info)
|
||||
{
|
||||
// Don't show every disconnected player as new, don't show 0-minute players as new if threshold is
|
||||
if (newPlayerThreshold <= 0 || info.OverallPlaytime is null && !info.Connected)
|
||||
return false;
|
||||
|
||||
return (info.OverallPlaytime is null
|
||||
|| info.OverallPlaytime < TimeSpan.FromMinutes(newPlayerThreshold));
|
||||
}
|
||||
|
||||
ChannelSelector.Comparison = (a, b) =>
|
||||
{
|
||||
var ach = AHelpHelper.EnsurePanel(a.SessionId);
|
||||
|
|
@ -93,31 +111,37 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
if (a.IsPinned != b.IsPinned)
|
||||
return a.IsPinned ? -1 : 1;
|
||||
|
||||
// First, sort by unread. Any chat with unread messages appears first.
|
||||
// Then, any chat with unread messages.
|
||||
var aUnread = ach.Unread > 0;
|
||||
var bUnread = bch.Unread > 0;
|
||||
if (aUnread != bUnread)
|
||||
return aUnread ? -1 : 1;
|
||||
|
||||
// Sort by recent messages during the current round.
|
||||
// Then, any chat with recent messages from the current round
|
||||
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
|
||||
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
|
||||
if (aRecent != bRecent)
|
||||
return aRecent ? -1 : 1;
|
||||
|
||||
// Next, sort by connection status. Any disconnected players are grouped towards the end.
|
||||
// Sort by connection status. Disconnected players will be last.
|
||||
if (a.Connected != b.Connected)
|
||||
return a.Connected ? -1 : 1;
|
||||
|
||||
// Sort connected players by New Player status, then by Antag status
|
||||
// Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
|
||||
if (a.Connected && b.Connected)
|
||||
{
|
||||
var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
|
||||
var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
|
||||
var aNewPlayer = IsNewPlayer(a);
|
||||
var bNewPlayer = IsNewPlayer(b);
|
||||
|
||||
// Players who have joined the round will be listed before players in the lobby
|
||||
if (a.ActiveThisRound != b.ActiveThisRound)
|
||||
return a.ActiveThisRound ? -1 : 1;
|
||||
|
||||
// Within both the joined group and lobby group, new players will be grouped and listed first
|
||||
if (aNewPlayer != bNewPlayer)
|
||||
return aNewPlayer ? -1 : 1;
|
||||
|
||||
// Within all four previous groups, antagonists will be listed first.
|
||||
if (a.Antag != b.Antag)
|
||||
return a.Antag ? -1 : 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,9 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||
return;
|
||||
}
|
||||
|
||||
Title = $"{sel.CharacterName} / {sel.Username}";
|
||||
Title = $"{sel.CharacterName} / {sel.Username} | {Loc.GetString("generic-playtime-title")}: ";
|
||||
|
||||
if (sel.OverallPlaytime != null)
|
||||
{
|
||||
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
|
||||
}
|
||||
Title += sel.OverallPlaytime != null ? sel.PlaytimeString : Loc.GetString("generic-unknown-title");
|
||||
};
|
||||
|
||||
OnOpen += () =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Advertise.Systems;
|
||||
|
||||
namespace Content.Client.Advertise.Systems;
|
||||
|
||||
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
|
||||
|
|
@ -28,7 +28,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
|||
private void OnMapInit(EntityUid uid, SolutionContainerVisualsComponent component, MapInitEvent args)
|
||||
{
|
||||
var meta = MetaData(uid);
|
||||
component.InitialName = meta.EntityName;
|
||||
component.InitialDescription = meta.EntityDescription;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,34 @@
|
|||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Horizontal"
|
||||
Orientation="Vertical"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 5">
|
||||
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
|
||||
<RichTextLabel Name="ReactantsLabel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Access="Public"
|
||||
Visible="False"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png"
|
||||
HorizontalAlignment="Center"
|
||||
Name="MixTexture"
|
||||
Access="Public"/>
|
||||
<RichTextLabel Name="MixLabel"
|
||||
HorizontalAlignment="Center"
|
||||
Access="Public"
|
||||
Margin="2 0 0 0"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
|
||||
<RichTextLabel Name="ProductsLabel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Access="Public"
|
||||
Visible="False"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True"
|
||||
VerticalAlignment="Center">
|
||||
<RichTextLabel Name="ReactantsLabel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Access="Public"
|
||||
Visible="False" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png"
|
||||
HorizontalAlignment="Center"
|
||||
Name="MixTexture"
|
||||
Access="Public" />
|
||||
<RichTextLabel Name="MixLabel"
|
||||
HorizontalAlignment="Center"
|
||||
Access="Public"
|
||||
Margin="2 0 0 0" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
|
||||
<RichTextLabel Name="ProductsLabel"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Access="Public"
|
||||
Visible="False" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Numerics; // CD - Character Records
|
||||
|
|
@ -14,12 +16,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
|
||||
Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
|
||||
|
|
@ -27,6 +32,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||
UpdateSprite(component, Comp<SpriteComponent>(uid));
|
||||
}
|
||||
|
||||
private void OnCvarChanged(bool value)
|
||||
{
|
||||
var humanoidQuery = EntityManager.AllEntityQueryEnumerator<HumanoidAppearanceComponent, SpriteComponent>();
|
||||
while (humanoidQuery.MoveNext(out var _, out var humanoidComp, out var spriteComp))
|
||||
{
|
||||
UpdateSprite(humanoidComp, spriteComp);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
|
||||
{
|
||||
UpdateLayers(component, sprite);
|
||||
|
|
@ -219,16 +233,30 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||
// Really, markings should probably be a separate component altogether.
|
||||
ClearAllMarkings(humanoid, sprite);
|
||||
|
||||
var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
|
||||
_configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
|
||||
// The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
|
||||
var applyUndergarmentTop = censorNudity;
|
||||
var applyUndergarmentBottom = censorNudity;
|
||||
|
||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
|
||||
{
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
|
||||
if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
|
||||
applyUndergarmentTop = false;
|
||||
else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
|
||||
applyUndergarmentBottom = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
|
||||
|
||||
AddUndergarments(humanoid, sprite, applyUndergarmentTop, applyUndergarmentBottom);
|
||||
}
|
||||
|
||||
private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
|
||||
|
|
@ -276,6 +304,31 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||
spriteComp.RemoveLayer(index);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteComponent sprite, bool undergarmentTop, bool undergarmentBottom)
|
||||
{
|
||||
if (undergarmentTop && humanoid.UndergarmentTop != null)
|
||||
{
|
||||
var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
// Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
|
||||
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking>{ marking });
|
||||
ApplyMarking(prototype, null, true, humanoid, sprite);
|
||||
}
|
||||
}
|
||||
|
||||
if (undergarmentBottom && humanoid.UndergarmentBottom != null)
|
||||
{
|
||||
var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
|
||||
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||
{
|
||||
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking>{ marking });
|
||||
ApplyMarking(prototype, null, true, humanoid, sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMarking(MarkingPrototype markingPrototype,
|
||||
IReadOnlyList<Color>? colors,
|
||||
bool visible,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
PlaceHolder="0"
|
||||
Text="1"
|
||||
HorizontalExpand="True" />
|
||||
<Label Name="RecipeCount" Margin="8 0 8 0" MinWidth="90" Align="Right" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ public sealed partial class LatheMenu : DefaultWindow
|
|||
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
|
||||
quantity = 1;
|
||||
|
||||
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
|
||||
|
||||
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
|
||||
RecipeList.Children.Clear();
|
||||
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
|
|||
|
||||
worldHandle.SetTransform(localMatrix);
|
||||
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
||||
}, null);
|
||||
}, Color.Transparent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
|
|||
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
|
||||
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
|
||||
_overlayMan.AddOverlay(new LightBlurOverlay());
|
||||
_overlayMan.AddOverlay(new SunShadowOverlay());
|
||||
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
|
|||
_overlayMan.RemoveOverlay<RoofOverlay>();
|
||||
_overlayMan.RemoveOverlay<TileEmissionOverlay>();
|
||||
_overlayMan.RemoveOverlay<LightBlurOverlay>();
|
||||
_overlayMan.RemoveOverlay<SunShadowOverlay>();
|
||||
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Light.EntitySystems;
|
||||
|
||||
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||
{
|
||||
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var mapQuery = AllEntityQuery<SunShadowCycleComponent, SunShadowComponent>();
|
||||
while (mapQuery.MoveNext(out var uid, out var cycle, out var shadow))
|
||||
{
|
||||
if (!cycle.Running || cycle.Directions.Count == 0)
|
||||
continue;
|
||||
|
||||
var pausedTime = _metadata.GetPauseTime(uid);
|
||||
|
||||
var time = (float)(_timing.CurTime
|
||||
.Add(cycle.Offset)
|
||||
.Subtract(_ticker.RoundStartTimeSpan)
|
||||
.Subtract(pausedTime)
|
||||
.TotalSeconds % cycle.Duration.TotalSeconds);
|
||||
|
||||
var (direction, alpha) = GetShadow((uid, cycle), time);
|
||||
shadow.Direction = direction;
|
||||
shadow.Alpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public (Vector2 Direction, float Alpha) GetShadow(Entity<SunShadowCycleComponent> entity, float time)
|
||||
{
|
||||
// So essentially the values are stored as the percentages of the total duration just so it adjusts the speed
|
||||
// dynamically and we don't have to manually handle it.
|
||||
// It will lerp from each value to the next one with angle and length handled separately
|
||||
var ratio = (float) (time / entity.Comp.Duration.TotalSeconds);
|
||||
|
||||
for (var i = entity.Comp.Directions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var dir = entity.Comp.Directions[i];
|
||||
|
||||
if (ratio > dir.Ratio)
|
||||
{
|
||||
var next = entity.Comp.Directions[(i + 1) % entity.Comp.Directions.Count];
|
||||
float nextRatio;
|
||||
|
||||
// Last entry
|
||||
if (i == entity.Comp.Directions.Count - 1)
|
||||
{
|
||||
nextRatio = next.Ratio + 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextRatio = next.Ratio;
|
||||
}
|
||||
|
||||
var range = nextRatio - dir.Ratio;
|
||||
var diff = (ratio - dir.Ratio) / range;
|
||||
DebugTools.Assert(diff is >= 0f and <= 1f);
|
||||
|
||||
// We lerp angle + length separately as we don't want a straight-line lerp and want the rotation to be consistent.
|
||||
var currentAngle = dir.Direction.ToAngle();
|
||||
var nextAngle = next.Direction.ToAngle();
|
||||
|
||||
var angle = Angle.Lerp(currentAngle, nextAngle, diff);
|
||||
// This is to avoid getting weird issues where the angle gets pretty close but length still noticeably catches up.
|
||||
var lengthDiff = MathF.Pow(diff, 1f / 2f);
|
||||
var length = float.Lerp(dir.Direction.Length(), next.Direction.Length(), lengthDiff);
|
||||
|
||||
var vector = angle.ToVec() * length;
|
||||
var alpha = float.Lerp(dir.Alpha, next.Alpha, diff);
|
||||
return (vector, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Shared;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
|
|
@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
|
|||
{
|
||||
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!_timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
|
||||
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
|
||||
{
|
||||
if (!cycle.Running)
|
||||
continue;
|
||||
|
||||
// We still iterate paused entities as we still want to override the lighting color and not have
|
||||
// it apply the server state
|
||||
var pausedTime = _metadata.GetPauseTime(uid);
|
||||
|
||||
var time = (float) _timing.CurTime
|
||||
.Add(cycle.Offset)
|
||||
.Subtract(_ticker.RoundStartTimeSpan)
|
||||
.Subtract(pausedTime)
|
||||
.TotalSeconds;
|
||||
|
||||
var color = GetColor((uid, cycle), cycle.OriginalColor, time);
|
||||
|
|
|
|||
|
|
@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
|
|||
// Due to stencilling we essentially draw on unrooved tiles
|
||||
while (tileEnumerator.MoveNext(out var tileRef))
|
||||
{
|
||||
if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
|
||||
var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
|
||||
|
||||
if (color == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
|
||||
worldHandle.DrawRect(local, roof.Color);
|
||||
worldHandle.DrawRect(local, color.Value);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
using System.Numerics;
|
||||
using Content.Shared.Light.Components;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Light;
|
||||
|
||||
public sealed class SunShadowOverlay : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
private readonly EntityLookupSystem _lookup;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
|
||||
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
|
||||
|
||||
private IRenderTexture? _blurTarget;
|
||||
private IRenderTexture? _target;
|
||||
|
||||
public SunShadowOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||
_lookup = _entManager.System<EntityLookupSystem>();
|
||||
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
|
||||
}
|
||||
|
||||
private List<Entity<MapGridComponent>> _grids = new();
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var viewport = args.Viewport;
|
||||
var eye = viewport.Eye;
|
||||
|
||||
if (eye == null)
|
||||
return;
|
||||
|
||||
_grids.Clear();
|
||||
_mapManager.FindGridsIntersecting(args.MapId,
|
||||
args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
|
||||
ref _grids);
|
||||
|
||||
var worldHandle = args.WorldHandle;
|
||||
var mapId = args.MapId;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var targetSize = viewport.LightRenderTarget.Size;
|
||||
|
||||
if (_target?.Size != targetSize)
|
||||
{
|
||||
_target = _clyde
|
||||
.CreateRenderTarget(targetSize,
|
||||
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||
name: "sun-shadow-target");
|
||||
|
||||
if (_blurTarget?.Size != targetSize)
|
||||
{
|
||||
_blurTarget = _clyde
|
||||
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
|
||||
}
|
||||
}
|
||||
|
||||
var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
|
||||
var scale = viewport.RenderScale / (Vector2.One / lightScale);
|
||||
|
||||
foreach (var grid in _grids)
|
||||
{
|
||||
if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var direction = sun.Direction;
|
||||
var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
|
||||
|
||||
// Nowhere to cast to so ignore it.
|
||||
if (direction.Equals(Vector2.Zero) || alpha == 0f)
|
||||
continue;
|
||||
|
||||
// Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
|
||||
// TODO: Jittering still not quite perfect
|
||||
|
||||
var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
|
||||
_shadows.Clear();
|
||||
|
||||
// Draw shadow polys to stencil
|
||||
args.WorldHandle.RenderInRenderTarget(_target,
|
||||
() =>
|
||||
{
|
||||
var invMatrix =
|
||||
_target.GetWorldToLocalMatrix(eye, scale);
|
||||
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
|
||||
|
||||
// Go through shadows in range.
|
||||
|
||||
// For each one we:
|
||||
// - Get the original vertices.
|
||||
// - Extrapolate these along the sun direction.
|
||||
// - Combine the above into 1 single polygon to draw.
|
||||
|
||||
// Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
|
||||
// This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
|
||||
// Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
|
||||
// You might need to batch verts or the likes as this could get expensive.
|
||||
_lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
|
||||
|
||||
foreach (var ent in _shadows)
|
||||
{
|
||||
var xform = _entManager.GetComponent<TransformComponent>(ent.Owner);
|
||||
var worldMatrix = _xformSys.GetWorldMatrix(xform);
|
||||
var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
|
||||
var pointCount = ent.Comp.Points.Length;
|
||||
|
||||
Array.Copy(ent.Comp.Points, indices, pointCount);
|
||||
|
||||
for (var i = 0; i < pointCount; i++)
|
||||
{
|
||||
indices[pointCount + i] = indices[i] + direction;
|
||||
}
|
||||
|
||||
var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
|
||||
worldHandle.SetTransform(renderMatrix);
|
||||
|
||||
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
|
||||
}
|
||||
},
|
||||
Color.Transparent);
|
||||
|
||||
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
|
||||
_clyde.BlurRenderTarget(viewport, _target, _target, eye, 1f);
|
||||
|
||||
// Draw stencil (see roofoverlay).
|
||||
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
|
||||
() =>
|
||||
{
|
||||
var invMatrix =
|
||||
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
|
||||
worldHandle.SetTransform(invMatrix);
|
||||
|
||||
var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
|
||||
worldHandle.UseShader(maskShader);
|
||||
|
||||
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
<BoxContainer Orientation="Vertical">
|
||||
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical" Margin="8">
|
||||
<Label Text="{Loc 'ui-options-accessability-header-visuals'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
|
||||
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
|
||||
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
|
||||
|
|
@ -12,6 +14,9 @@
|
|||
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
|
||||
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
|
||||
<Label Text="{Loc 'ui-options-accessability-header-content'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="CensorNudityCheckBox" Text="{Loc 'ui-options-censor-nudity'}" />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ public sealed partial class AccessibilityTab : Control
|
|||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
|
||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
|
||||
|
||||
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,6 +298,51 @@ namespace Content.Client.Options.UI.Tabs
|
|||
AddButton(EngineKeyFunctions.HideUI);
|
||||
AddButton(ContentKeyFunctions.InspectEntity);
|
||||
|
||||
AddHeader("ui-options-header-text-cursor");
|
||||
AddButton(EngineKeyFunctions.TextCursorLeft);
|
||||
AddButton(EngineKeyFunctions.TextCursorRight);
|
||||
AddButton(EngineKeyFunctions.TextCursorUp);
|
||||
AddButton(EngineKeyFunctions.TextCursorDown);
|
||||
AddButton(EngineKeyFunctions.TextCursorWordLeft);
|
||||
AddButton(EngineKeyFunctions.TextCursorWordRight);
|
||||
AddButton(EngineKeyFunctions.TextCursorBegin);
|
||||
AddButton(EngineKeyFunctions.TextCursorEnd);
|
||||
|
||||
AddHeader("ui-options-header-text-cursor-select");
|
||||
AddButton(EngineKeyFunctions.TextCursorSelect);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectLeft);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectRight);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectUp);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectDown);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectWordLeft);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectWordRight);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectBegin);
|
||||
AddButton(EngineKeyFunctions.TextCursorSelectEnd);
|
||||
|
||||
AddHeader("ui-options-header-text-edit");
|
||||
AddButton(EngineKeyFunctions.TextBackspace);
|
||||
AddButton(EngineKeyFunctions.TextDelete);
|
||||
AddButton(EngineKeyFunctions.TextWordBackspace);
|
||||
AddButton(EngineKeyFunctions.TextWordDelete);
|
||||
AddButton(EngineKeyFunctions.TextNewline);
|
||||
AddButton(EngineKeyFunctions.TextSubmit);
|
||||
AddButton(EngineKeyFunctions.MultilineTextSubmit);
|
||||
AddButton(EngineKeyFunctions.TextSelectAll);
|
||||
AddButton(EngineKeyFunctions.TextCopy);
|
||||
AddButton(EngineKeyFunctions.TextCut);
|
||||
AddButton(EngineKeyFunctions.TextPaste);
|
||||
|
||||
AddHeader("ui-options-header-text-chat");
|
||||
AddButton(EngineKeyFunctions.TextHistoryPrev);
|
||||
AddButton(EngineKeyFunctions.TextHistoryNext);
|
||||
AddButton(EngineKeyFunctions.TextReleaseFocus);
|
||||
AddButton(EngineKeyFunctions.TextScrollToBottom);
|
||||
|
||||
AddHeader("ui-options-header-text-other");
|
||||
AddButton(EngineKeyFunctions.TextTabComplete);
|
||||
AddButton(EngineKeyFunctions.TextCompleteNext);
|
||||
AddButton(EngineKeyFunctions.TextCompletePrev);
|
||||
|
||||
foreach (var control in _keyControls.Values)
|
||||
{
|
||||
UpdateKeyControl(control);
|
||||
|
|
|
|||
|
|
@ -96,9 +96,12 @@ public class ListContainer : Control
|
|||
{
|
||||
ListContainerButton control = new(data[0], 0);
|
||||
GenerateItem?.Invoke(data[0], control);
|
||||
// Yes this AddChild is necessary for reasons (get proper style or whatever?)
|
||||
// without it the DesiredSize may be different to the final DesiredSize.
|
||||
AddChild(control);
|
||||
control.Measure(Vector2Helpers.Infinity);
|
||||
_itemHeight = control.DesiredSize.Y;
|
||||
control.Dispose();
|
||||
control.Orphan();
|
||||
}
|
||||
|
||||
// Ensure buttons are re-generated.
|
||||
|
|
@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
|
|||
|
||||
public ListContainerButton(ListData data, int index)
|
||||
{
|
||||
AddStyleClass(StyleClassButton);
|
||||
Data = data;
|
||||
Index = index;
|
||||
// AddChild(Background = new PanelContainer
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public sealed class DamageOverlayUiController : UIController
|
|||
{
|
||||
_overlay.DeadLevel = 0f;
|
||||
_overlay.CritLevel = 0f;
|
||||
_overlay.BruteLevel = 0f;
|
||||
_overlay.PainLevel = 0f;
|
||||
_overlay.OxygenLevel = 0f;
|
||||
}
|
||||
|
||||
|
|
@ -95,13 +95,22 @@ public sealed class DamageOverlayUiController : UIController
|
|||
{
|
||||
case MobState.Alive:
|
||||
{
|
||||
if (EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
||||
FixedPoint2 painLevel = 0;
|
||||
_overlay.PainLevel = 0;
|
||||
|
||||
if (!EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
||||
{
|
||||
_overlay.BruteLevel = 0;
|
||||
}
|
||||
else if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
|
||||
{
|
||||
_overlay.BruteLevel = FixedPoint2.Min(1f, bruteDamage / critThreshold).Float();
|
||||
foreach (var painDamageType in damageable.PainDamageGroups)
|
||||
{
|
||||
damageable.DamagePerGroup.TryGetValue(painDamageType, out var painDamage);
|
||||
painLevel += painDamage;
|
||||
}
|
||||
_overlay.PainLevel = FixedPoint2.Min(1f, painLevel / critThreshold).Float();
|
||||
|
||||
if (_overlay.PainLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
|
||||
{
|
||||
_overlay.PainLevel = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (damageable.DamagePerGroup.TryGetValue("Airloss", out var oxyDamage))
|
||||
|
|
@ -109,11 +118,6 @@ public sealed class DamageOverlayUiController : UIController
|
|||
_overlay.OxygenLevel = FixedPoint2.Min(1f, oxyDamage / critThreshold).Float();
|
||||
}
|
||||
|
||||
if (_overlay.BruteLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
|
||||
{
|
||||
_overlay.BruteLevel = 0;
|
||||
}
|
||||
|
||||
_overlay.CritLevel = 0;
|
||||
_overlay.DeadLevel = 0;
|
||||
break;
|
||||
|
|
@ -125,13 +129,13 @@ public sealed class DamageOverlayUiController : UIController
|
|||
return;
|
||||
_overlay.CritLevel = critLevel.Value.Float();
|
||||
|
||||
_overlay.BruteLevel = 0;
|
||||
_overlay.PainLevel = 0;
|
||||
_overlay.DeadLevel = 0;
|
||||
break;
|
||||
}
|
||||
case MobState.Dead:
|
||||
{
|
||||
_overlay.BruteLevel = 0;
|
||||
_overlay.PainLevel = 0;
|
||||
_overlay.CritLevel = 0;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ public sealed class DamageOverlay : Overlay
|
|||
/// <summary>
|
||||
/// Handles the red pulsing overlay
|
||||
/// </summary>
|
||||
public float BruteLevel = 0f;
|
||||
public float PainLevel = 0f;
|
||||
|
||||
private float _oldBruteLevel = 0f;
|
||||
private float _oldPainLevel = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the darkening overlay.
|
||||
|
|
@ -92,14 +92,14 @@ public sealed class DamageOverlay : Overlay
|
|||
DeadLevel = 0f;
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseTo(_oldBruteLevel, BruteLevel, 0.001f))
|
||||
if (!MathHelper.CloseTo(_oldPainLevel, PainLevel, 0.001f))
|
||||
{
|
||||
var diff = BruteLevel - _oldBruteLevel;
|
||||
_oldBruteLevel += GetDiff(diff, lastFrameTime);
|
||||
var diff = PainLevel - _oldPainLevel;
|
||||
_oldPainLevel += GetDiff(diff, lastFrameTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
_oldBruteLevel = BruteLevel;
|
||||
_oldPainLevel = PainLevel;
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
|
||||
|
|
@ -135,7 +135,7 @@ public sealed class DamageOverlay : Overlay
|
|||
|
||||
// Makes debugging easier don't @ me
|
||||
float level = 0f;
|
||||
level = _oldBruteLevel;
|
||||
level = _oldPainLevel;
|
||||
|
||||
// TODO: Lerping
|
||||
if (level > 0f && _oldCritLevel <= 0f)
|
||||
|
|
@ -165,7 +165,7 @@ public sealed class DamageOverlay : Overlay
|
|||
}
|
||||
else
|
||||
{
|
||||
_oldBruteLevel = BruteLevel;
|
||||
_oldPainLevel = PainLevel;
|
||||
}
|
||||
|
||||
level = State != MobState.Critical ? _oldOxygenLevel : 1f;
|
||||
|
|
|
|||
|
|
@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
|
|||
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Robust.Client.AutoGenerated;
|
||||
|
|
@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI
|
|||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
|
||||
private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
|
||||
private readonly Dictionary<EntProtoId, uint> _amounts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vending machine is able to be interacted with or not.
|
||||
/// </summary>
|
||||
private bool _enabled;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
||||
|
||||
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
|
||||
|
||||
public VendingMachineMenu()
|
||||
{
|
||||
MinSize = SetSize = new Vector2(250, 150);
|
||||
|
|
@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
|
|||
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
|
||||
return;
|
||||
|
||||
button.AddChild(new VendingMachineItem(protoID, text));
|
||||
|
||||
button.ToolTip = text;
|
||||
button.StyleBoxOverride = _styleBox;
|
||||
var item = new VendingMachineItem(protoID, text);
|
||||
_listItems[protoID] = (button, item);
|
||||
button.AddChild(item);
|
||||
button.AddStyleClass("ButtonSquare");
|
||||
button.Disabled = !_enabled || _amounts[protoID] == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the list of available items on the vending machine interface
|
||||
/// and sets icons based on their prototypes
|
||||
/// </summary>
|
||||
public void Populate(List<VendingMachineInventoryEntry> inventory)
|
||||
public void Populate(List<VendingMachineInventoryEntry> inventory, bool enabled)
|
||||
{
|
||||
_enabled = enabled;
|
||||
_listItems.Clear();
|
||||
_amounts.Clear();
|
||||
|
||||
if (inventory.Count == 0 && VendingContents.Visible)
|
||||
{
|
||||
SearchBar.Visible = false;
|
||||
|
|
@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
|
|||
var entry = inventory[i];
|
||||
|
||||
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
|
||||
{
|
||||
_amounts[entry.ID] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_dummies.TryGetValue(entry.ID, out var dummy))
|
||||
{
|
||||
|
|
@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
|
|||
|
||||
var itemName = Identity.Name(dummy, _entityManager);
|
||||
var itemText = $"{itemName} [{entry.Amount}]";
|
||||
_amounts[entry.ID] = entry.Amount;
|
||||
|
||||
if (itemText.Length > longestEntry.Length)
|
||||
longestEntry = itemText;
|
||||
|
||||
listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
|
||||
listData.Add(new VendorItemsListData(prototype.ID, i)
|
||||
{
|
||||
ItemText = itemText,
|
||||
});
|
||||
}
|
||||
|
||||
VendingContents.PopulateList(listData);
|
||||
|
|
@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
|
|||
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates text entries for vending data in place without modifying the list controls.
|
||||
/// </summary>
|
||||
public void UpdateAmounts(List<VendingMachineInventoryEntry> cachedInventory, bool enabled)
|
||||
{
|
||||
_enabled = enabled;
|
||||
|
||||
foreach (var proto in _dummies.Keys)
|
||||
{
|
||||
if (!_listItems.TryGetValue(proto, out var button))
|
||||
continue;
|
||||
|
||||
var dummy = _dummies[proto];
|
||||
var amount = cachedInventory.First(o => o.ID == proto).Amount;
|
||||
// Could be better? Problem is all inventory entries get squashed.
|
||||
var text = GetItemText(dummy, amount);
|
||||
|
||||
button.Item.SetText(text);
|
||||
button.Button.Disabled = !enabled || amount == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetItemText(EntityUid dummy, uint amount)
|
||||
{
|
||||
var itemName = Identity.Name(dummy, _entityManager);
|
||||
return $"{itemName} [{amount}]";
|
||||
}
|
||||
|
||||
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
|
||||
{
|
||||
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
|
||||
Math.Clamp(contentCount * 50, 150, 350));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
|
||||
public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
|
||||
{
|
||||
public string ItemText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
|
|||
|
||||
public void Refresh()
|
||||
{
|
||||
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
|
||||
|
||||
var system = EntMan.System<VendingMachineSystem>();
|
||||
_cachedInventory = system.GetAllInventory(Owner);
|
||||
|
||||
_menu?.Populate(_cachedInventory);
|
||||
_menu?.Populate(_cachedInventory, enabled);
|
||||
}
|
||||
|
||||
public void UpdateAmounts()
|
||||
{
|
||||
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
|
||||
|
||||
var system = EntMan.System<VendingMachineSystem>();
|
||||
_cachedInventory = system.GetAllInventory(Owner);
|
||||
_menu?.UpdateAmounts(_cachedInventory, enabled);
|
||||
}
|
||||
|
||||
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
|
||||
|
|
@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
|
|||
if (selectedItem == null)
|
||||
return;
|
||||
|
||||
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
|
||||
SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.VendingMachines;
|
||||
|
||||
|
|
@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
|||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -16,14 +17,69 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
|||
|
||||
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
|
||||
SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
|
||||
}
|
||||
|
||||
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
|
||||
private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
|
||||
{
|
||||
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
|
||||
if (args.Current is not VendingMachineComponentState state)
|
||||
return;
|
||||
|
||||
var uid = entity.Owner;
|
||||
var component = entity.Comp;
|
||||
|
||||
component.Contraband = state.Contraband;
|
||||
component.EjectEnd = state.EjectEnd;
|
||||
component.DenyEnd = state.DenyEnd;
|
||||
component.DispenseOnHitEnd = state.DispenseOnHitEnd;
|
||||
|
||||
// If all we did was update amounts then we can leave BUI buttons in place.
|
||||
var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
|
||||
!component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
|
||||
!component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
|
||||
|
||||
component.Inventory.Clear();
|
||||
component.EmaggedInventory.Clear();
|
||||
component.ContrabandInventory.Clear();
|
||||
|
||||
foreach (var entry in state.Inventory)
|
||||
{
|
||||
bui.Refresh();
|
||||
component.Inventory.Add(entry.Key, new(entry.Value));
|
||||
}
|
||||
|
||||
foreach (var entry in state.EmaggedInventory)
|
||||
{
|
||||
component.EmaggedInventory.Add(entry.Key, new(entry.Value));
|
||||
}
|
||||
|
||||
foreach (var entry in state.ContrabandInventory)
|
||||
{
|
||||
component.ContrabandInventory.Add(entry.Key, new(entry.Value));
|
||||
}
|
||||
|
||||
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
|
||||
{
|
||||
if (fullUiUpdate)
|
||||
{
|
||||
bui.Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
bui.UpdateAmounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(entity.Owner,
|
||||
VendingMachineUiKey.Key,
|
||||
out var bui))
|
||||
{
|
||||
bui.UpdateAmounts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
|||
if (component.LoopDenyAnimation)
|
||||
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
|
||||
else
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
|
||||
|
||||
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
||||
break;
|
||||
|
||||
case VendingMachineVisualState.Eject:
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
|
||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
|
||||
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
||||
break;
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ public sealed class CraftingTests : InteractionTest
|
|||
await CraftItem(Spear);
|
||||
await FindEntity(Spear);
|
||||
|
||||
// Reset target because entitylookup will dump this.
|
||||
Target = null;
|
||||
|
||||
// Player's hands should be full of the remaining rods, except those dropped during the failed crafting attempt.
|
||||
// Spear and left over stacks should be on the floor.
|
||||
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
|
||||
|
|
|
|||
|
|
@ -17,23 +17,26 @@ public sealed class ContrabandTest
|
|||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||
continue;
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
|
||||
continue;
|
||||
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
|
||||
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
|
||||
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
|
||||
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
|
||||
|
||||
if (!severity.ShowDepartmentsAndJobs)
|
||||
continue;
|
||||
if (!severity.ShowDepartmentsAndJobs)
|
||||
continue;
|
||||
|
||||
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
|
||||
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
|
||||
}
|
||||
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
|
||||
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ public abstract partial class InteractionTest
|
|||
|
||||
// Please someone purge async construction code
|
||||
Task<bool> task = default!;
|
||||
await Server.WaitPost(() => task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player)));
|
||||
await Server.WaitPost(() =>
|
||||
{
|
||||
task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
|
||||
});
|
||||
|
||||
Task? tickTask = null;
|
||||
while (!task.IsCompleted)
|
||||
|
|
|
|||
|
|
@ -23,46 +23,49 @@ public sealed class MagazineVisualsSpriteTest
|
|||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
|
||||
continue;
|
||||
|
||||
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
|
||||
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
|
||||
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
|
||||
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
|
||||
|
||||
var toTest = new List<(int, string)>();
|
||||
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
|
||||
toTest.Add((magLayerId, ""));
|
||||
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
|
||||
toTest.Add((magUnshadedLayerId, "-unshaded"));
|
||||
|
||||
Assert.That(toTest, Is.Not.Empty,
|
||||
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
|
||||
|
||||
var start = visuals.ZeroVisible ? 0 : 1;
|
||||
foreach (var (id, midfix) in toTest)
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
Assert.That(sprite.TryGetLayer(id, out var layer));
|
||||
var rsi = layer.ActualRsi;
|
||||
for (var i = start; i < visuals.MagSteps; i++)
|
||||
{
|
||||
var state = $"{visuals.MagState}{midfix}-{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _),
|
||||
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
|
||||
}
|
||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
// MagSteps includes the 0th step, so sometimes people are off by one.
|
||||
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
|
||||
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
|
||||
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
|
||||
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
|
||||
continue;
|
||||
|
||||
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
|
||||
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
|
||||
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
|
||||
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
|
||||
|
||||
var toTest = new List<(int, string)>();
|
||||
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
|
||||
toTest.Add((magLayerId, ""));
|
||||
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
|
||||
toTest.Add((magUnshadedLayerId, "-unshaded"));
|
||||
|
||||
Assert.That(toTest, Is.Not.Empty,
|
||||
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
|
||||
|
||||
var start = visuals.ZeroVisible ? 0 : 1;
|
||||
foreach (var (id, midfix) in toTest)
|
||||
{
|
||||
Assert.That(sprite.TryGetLayer(id, out var layer));
|
||||
var rsi = layer.ActualRsi;
|
||||
for (var i = start; i < visuals.MagSteps; i++)
|
||||
{
|
||||
var state = $"{visuals.MagState}{midfix}-{i}";
|
||||
Assert.That(rsi.TryGetState(state, out _),
|
||||
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
|
||||
}
|
||||
|
||||
// MagSteps includes the 0th step, so sometimes people are off by one.
|
||||
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
|
||||
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
|
||||
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
using Content.Server._DV.Cloning;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests._DV;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class MetempsychosisTest
|
||||
{
|
||||
[Test]
|
||||
public async Task AllHumanoidPoolSpeciesExist()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
// Per RobustIntegrationTest.cs, wait until state is settled to access it.
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
var metemComponent = new MetempsychoticMachineComponent();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
prototypeManager.TryIndex(metemComponent.MetempsychoticHumanoidPool,
|
||||
out var humanoidPool);
|
||||
prototypeManager.TryIndex(metemComponent.MetempsychoticNonHumanoidPool,
|
||||
out var nonHumanoidPool);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(humanoidPool, Is.Not.Null, "MetempsychoticHumanoidPool is null!");
|
||||
Assert.That(nonHumanoidPool, Is.Not.Null, "MetempsychoticNonHumanoidPool is null!");
|
||||
Assert.That(humanoidPool.Weights,
|
||||
Is.Not.Empty,
|
||||
"MetempsychoticHumanoidPool has no valid prototypes!");
|
||||
Assert.That(nonHumanoidPool.Weights,
|
||||
Is.Not.Empty,
|
||||
"MetempsychoticNonHumanoidPool has no valid prototypes!");
|
||||
});
|
||||
|
||||
foreach (var key in humanoidPool.Weights.Keys)
|
||||
{
|
||||
Assert.That(prototypeManager.TryIndex<SpeciesPrototype>(key, out _),
|
||||
$"MetempsychoticHumanoidPool has invalid prototype {key}!");
|
||||
}
|
||||
|
||||
foreach (var key in nonHumanoidPool.Weights.Keys)
|
||||
{
|
||||
Assert.That(prototypeManager.TryIndex<EntityPrototype>(key, out _),
|
||||
$"MetempsychoticNonHumanoidPool has invalid prototype {key}!");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -78,7 +78,12 @@ public sealed class IdCardSystem : SharedIdCardSystem
|
|||
}
|
||||
|
||||
// Give them a wonderful new access to compensate for everything
|
||||
var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
|
||||
var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
|
||||
|
||||
if (ids.Length == 0)
|
||||
return;
|
||||
|
||||
var random = _random.Pick(ids);
|
||||
|
||||
access.Tags.Add(random.ID);
|
||||
Dirty(uid, access);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
using Content.Server.GameTicking;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class ForceGhostCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||
|
||||
public override string Command => "forceghost";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0 || args.Length > 1)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playerManager.TryGetSessionByUsername(args[0], out var player))
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
|
||||
playerStatus is not PlayerGameStatus.JoinedGame)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-forceghost-error-lobby"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_mind.TryGetMind(player, out var mindId, out var mind))
|
||||
(mindId, mind) = _mind.CreateMind(player.UserId);
|
||||
|
||||
if (!_ghost.OnGhostAttempt(mindId, false, true, true, mind))
|
||||
shell.WriteLine(Loc.GetString("cmd-forceghost-denied"));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-forceghost-hint"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -222,6 +222,7 @@ public sealed class AdminSystem : EntitySystem
|
|||
var entityName = string.Empty;
|
||||
var identityName = string.Empty;
|
||||
|
||||
// Visible (identity) name can be different from real name
|
||||
if (session?.AttachedEntity != null)
|
||||
{
|
||||
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
||||
|
|
@ -230,6 +231,7 @@ public sealed class AdminSystem : EntitySystem
|
|||
|
||||
var antag = false;
|
||||
|
||||
// Starting role, antagonist status and role type
|
||||
RoleTypePrototype roleType = new();
|
||||
var startingRole = string.Empty;
|
||||
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
|
||||
|
|
@ -243,8 +245,13 @@ public sealed class AdminSystem : EntitySystem
|
|||
startingRole = _jobs.MindTryGetJobName(mindId);
|
||||
}
|
||||
|
||||
// Connection status and playtime
|
||||
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
||||
TimeSpan? overallPlaytime = null;
|
||||
|
||||
// Start with the last available playtime data
|
||||
var cachedInfo = GetCachedPlayerInfo(data.UserId);
|
||||
var overallPlaytime = cachedInfo?.OverallPlaytime;
|
||||
// Overwrite with current playtime data, unless it's null (such as if the player just disconnected)
|
||||
if (session != null &&
|
||||
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
||||
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Advertise.Systems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
|
||||
|
||||
namespace Content.Server.Advertise;
|
||||
namespace Content.Server.Advertise.EntitySystems;
|
||||
|
||||
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
||||
public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
|
@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
|||
entity.Comp.Flag = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return false;
|
||||
|
||||
entity.Comp.Flag = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
using Content.Server.Power.Components;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Advertise.EntitySystems;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Arcade;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Arcade.BlockGame;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using Content.Server.Power.Components;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Advertise.EntitySystems;
|
||||
using Content.Shared.Advertise.Components;
|
||||
using Content.Shared.Arcade;
|
||||
using Content.Shared.Power;
|
||||
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
|
@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
|
||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
|
||||
}
|
||||
|
||||
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
|
||||
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
|
||||
{
|
||||
if (component.Game == null)
|
||||
return;
|
||||
|
|
@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||
|
||||
switch (msg.PlayerAction)
|
||||
{
|
||||
case PlayerAction.Attack:
|
||||
case PlayerAction.Heal:
|
||||
case PlayerAction.Recharge:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
|
||||
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
|
||||
// Any sort of gameplay action counts
|
||||
if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
|
||||
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
|
||||
break;
|
||||
case PlayerAction.NewGame:
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
|
||||
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
|
||||
|
||||
component.Game = new SpaceVillainGame(uid, component, this);
|
||||
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
break;
|
||||
case PlayerAction.RequestData:
|
||||
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
|
||||
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
|
||||
return;
|
||||
|
||||
_uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
|
||||
_uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using Content.Server.Body.Components;
|
||||
using Content.Server.EntityEffects.Effects;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
|
|
@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
||||
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
||||
|
||||
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
|
||||
|
||||
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
|
||||
{
|
||||
donorComp.DNA = _forensicsSystem.GenerateDNA();
|
||||
|
||||
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
|
||||
RaiseLocalEvent(entity.Owner, ref ev);
|
||||
}
|
||||
|
||||
// Fill blood solution with BLOOD
|
||||
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
|
||||
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
}
|
||||
|
||||
|
|
@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
||||
}
|
||||
}
|
||||
else
|
||||
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||
var bloodData = new List<ReagentData>();
|
||||
var dnaData = new DnaData();
|
||||
|
||||
if (TryComp<DnaComponent>(uid, out var donorComp))
|
||||
{
|
||||
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
|
||||
dnaData.DNA = donorComp.DNA;
|
||||
} else
|
||||
{
|
||||
else
|
||||
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
||||
}
|
||||
|
||||
bloodData.Add(dnaData);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ namespace Content.Server.Cloning
|
|||
{
|
||||
private readonly EntityUid _mindId;
|
||||
private readonly MindComponent _mind;
|
||||
private readonly CloningSystem _cloningSystem;
|
||||
private readonly CloningPodSystem _cloningPodSystem;
|
||||
|
||||
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys)
|
||||
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
|
||||
{
|
||||
_mindId = mindId;
|
||||
_mind = mind;
|
||||
_cloningSystem = cloningSys;
|
||||
_cloningPodSystem = cloningPodSys;
|
||||
}
|
||||
|
||||
public override void HandleMessage(EuiMessageBase msg)
|
||||
|
|
@ -29,7 +29,7 @@ namespace Content.Server.Cloning
|
|||
return;
|
||||
}
|
||||
|
||||
_cloningSystem.TransferMindToClone(_mindId, _mind);
|
||||
_cloningPodSystem.TransferMindToClone(_mindId, _mind);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
|
|||
using Content.Server.Cloning.Components;
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Server.Medical.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Cloning;
|
||||
|
|
@ -16,24 +15,22 @@ using Content.Shared.Mind;
|
|||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Power;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
|
||||
namespace Content.Server.Cloning
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class CloningConsoleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly CloningSystem _cloningSystem = default!;
|
||||
[Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
|
@ -170,8 +167,7 @@ namespace Content.Server.Cloning
|
|||
|
||||
if (mind.UserId.HasValue == false || mind.Session == null)
|
||||
return;
|
||||
// Nyano: Adds scannerComp.MetemKarmaBonus
|
||||
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier, scannerComp.MetemKarmaBonus))
|
||||
if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,330 @@
|
|||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Cloning.Components;
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Materials;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Psionics; // DeltaV
|
||||
using Content.Shared._EE.Silicon.Components; // Goobstation
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DeviceLinking.Events;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Cloning;
|
||||
|
||||
public sealed class CloningPodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||
[Dependency] private readonly EuiManager _euiManager = null!;
|
||||
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
|
||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
||||
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
|
||||
public const float EasyModeCloningCost = 0.7f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
||||
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
||||
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
||||
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
||||
}
|
||||
|
||||
private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent.Owner, "clonepod-bodyContainer");
|
||||
_signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
|
||||
}
|
||||
|
||||
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
|
||||
!EntityManager.EntityExists(entity) ||
|
||||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
|
||||
mindComp.Mind != null)
|
||||
return;
|
||||
|
||||
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
|
||||
_mindSystem.UnVisit(mindId, mind);
|
||||
ClonesWaitingForMind.Remove(mind);
|
||||
}
|
||||
|
||||
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
|
||||
{
|
||||
if (clonedComponent.Parent == EntityUid.Invalid ||
|
||||
!EntityManager.EntityExists(clonedComponent.Parent) ||
|
||||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
||||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
||||
{
|
||||
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
||||
return;
|
||||
}
|
||||
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
||||
}
|
||||
private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
|
||||
{
|
||||
ent.Comp.ConnectedConsole = null;
|
||||
}
|
||||
|
||||
private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(ent.Comp.ConnectedConsole, out var console))
|
||||
return;
|
||||
|
||||
if (args.Anchored)
|
||||
{
|
||||
_cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
|
||||
return;
|
||||
}
|
||||
_cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<CloningPodComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
|
||||
return;
|
||||
|
||||
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
|
||||
}
|
||||
|
||||
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return false;
|
||||
|
||||
if (HasComp<ActiveCloningPodComponent>(uid))
|
||||
return false;
|
||||
|
||||
var mind = mindEnt.Comp;
|
||||
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
||||
{
|
||||
if (EntityManager.EntityExists(clone) &&
|
||||
!_mobStateSystem.IsDead(clone) &&
|
||||
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
||||
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
||||
return false; // Mind already has clone
|
||||
|
||||
ClonesWaitingForMind.Remove(mind);
|
||||
}
|
||||
|
||||
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
||||
return false; // Body controlled by mind is not dead
|
||||
|
||||
// Yes, we still need to track down the client because we need to open the Eui
|
||||
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
|
||||
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
|
||||
|
||||
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
|
||||
return false;
|
||||
|
||||
if (HasComp<SiliconComponent>(bodyToClone))
|
||||
return false; // Goobstation: Don't clone IPCs.
|
||||
|
||||
var cloningCost = (int)Math.Round(physics.FixturesMass);
|
||||
|
||||
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
|
||||
cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
|
||||
|
||||
// biomass checks
|
||||
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
|
||||
|
||||
if (biomassAmount < cloningCost)
|
||||
{
|
||||
if (clonePod.ConnectedConsole != null)
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// end of biomass checks
|
||||
|
||||
// genetic damage checks
|
||||
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
|
||||
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
|
||||
{
|
||||
var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
|
||||
chance *= failChanceModifier;
|
||||
|
||||
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
|
||||
|
||||
if (_robustRandom.Prob(chance))
|
||||
{
|
||||
clonePod.FailedClone = true;
|
||||
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
||||
AddComp<ActiveCloningPodComponent>(uid);
|
||||
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||
clonePod.UsedBiomass = cloningCost;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// end of genetic damage checks
|
||||
|
||||
if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
|
||||
{
|
||||
if (clonePod.ConnectedConsole != null)
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureComp<PotentialPsionicComponent>(mob.Value); // DeltaV
|
||||
|
||||
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
|
||||
cloneMindReturn.Mind = mind;
|
||||
cloneMindReturn.Parent = uid;
|
||||
_containerSystem.Insert(mob.Value, clonePod.BodyContainer);
|
||||
ClonesWaitingForMind.Add(mind, mob.Value);
|
||||
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
|
||||
|
||||
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
|
||||
AddComp<ActiveCloningPodComponent>(uid);
|
||||
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||
clonePod.UsedBiomass = cloningCost;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
||||
{
|
||||
cloningPod.Status = status;
|
||||
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
||||
while (query.MoveNext(out var uid, out var _, out var cloning))
|
||||
{
|
||||
if (!_powerReceiverSystem.IsPowered(uid))
|
||||
continue;
|
||||
|
||||
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
||||
continue;
|
||||
|
||||
cloning.CloningProgress += frameTime;
|
||||
if (cloning.CloningProgress < cloning.CloningTime)
|
||||
continue;
|
||||
|
||||
if (cloning.FailedClone)
|
||||
EndFailedCloning(uid, cloning);
|
||||
else
|
||||
Eject(uid, cloning);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
||||
/// </summary>
|
||||
private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(ent.Owner, EntityManager))
|
||||
return;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return;
|
||||
|
||||
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
|
||||
return;
|
||||
|
||||
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
|
||||
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
||||
clonePod.CloningProgress = 0f;
|
||||
clonePod.UsedBiomass = 0;
|
||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||
}
|
||||
|
||||
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
|
||||
{
|
||||
clonePod.FailedClone = false;
|
||||
clonePod.CloningProgress = 0f;
|
||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||
var transform = Transform(uid);
|
||||
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
|
||||
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
|
||||
|
||||
if (HasComp<EmaggedComponent>(uid))
|
||||
{
|
||||
_audio.PlayPvs(clonePod.ScreamSound, uid);
|
||||
Spawn(clonePod.MobSpawnId, transform.Coordinates);
|
||||
}
|
||||
|
||||
Solution bloodSolution = new();
|
||||
|
||||
var i = 0;
|
||||
while (i < 1)
|
||||
{
|
||||
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
|
||||
bloodSolution.AddReagent("Blood", 50);
|
||||
if (_robustRandom.Prob(0.2f))
|
||||
i++;
|
||||
}
|
||||
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
|
||||
|
||||
if (!HasComp<EmaggedComponent>(uid))
|
||||
{
|
||||
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||
}
|
||||
|
||||
clonePod.UsedBiomass = 0;
|
||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
ClonesWaitingForMind.Clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,382 +1,123 @@
|
|||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Cloning.Components;
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Jobs;
|
||||
using Content.Server.Materials;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared._EE.Silicon.Components; // Goobstation
|
||||
using Content.Server.Psionics; // DeltaV
|
||||
using Content.Server.Traits.Assorted; // DeltaV
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DeviceLinking.Events;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.NameModifier.Components;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Cloning
|
||||
namespace Content.Server.Cloning;
|
||||
|
||||
/// <summary>
|
||||
/// System responsible for making a copy of a humanoid's body.
|
||||
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
|
||||
/// </summary>
|
||||
public sealed class CloningSystem : EntitySystem
|
||||
{
|
||||
public sealed partial class CloningSystem : EntitySystem // DeltaV - Set to partial, see CloningSystem.Metempsychosis.cs
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
|
||||
/// </summary>
|
||||
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
|
||||
{
|
||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly EuiManager _euiManager = null!;
|
||||
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
clone = null;
|
||||
if (!_prototype.TryIndex(settingsId, out var settings))
|
||||
return false; // invalid settings
|
||||
|
||||
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
||||
public const float EasyModeCloningCost = 0.7f;
|
||||
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
|
||||
return false; // whatever body was to be cloned, was not a humanoid
|
||||
|
||||
public override void Initialize()
|
||||
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
||||
return false; // invalid species
|
||||
|
||||
var attemptEv = new CloningAttemptEvent(settings);
|
||||
RaiseLocalEvent(original, ref attemptEv);
|
||||
if (attemptEv.Cancelled && !settings.ForceCloning)
|
||||
return false; // cannot clone, for example due to the unrevivable trait
|
||||
|
||||
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
|
||||
_humanoidSystem.CloneAppearance(original, clone.Value);
|
||||
|
||||
var componentsToCopy = settings.Components;
|
||||
|
||||
// don't make status effects permanent
|
||||
if (TryComp<StatusEffectsComponent>(original, out var statusComp))
|
||||
componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
|
||||
|
||||
foreach (var componentName in componentsToCopy)
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
||||
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
||||
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
||||
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
|
||||
{
|
||||
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
|
||||
_signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
|
||||
}
|
||||
|
||||
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
|
||||
!EntityManager.EntityExists(entity) ||
|
||||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
|
||||
mindComp.Mind != null)
|
||||
return;
|
||||
|
||||
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
|
||||
_mindSystem.UnVisit(mindId, mind);
|
||||
ClonesWaitingForMind.Remove(mind);
|
||||
}
|
||||
|
||||
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
|
||||
{
|
||||
if (clonedComponent.Parent == EntityUid.Invalid ||
|
||||
!EntityManager.EntityExists(clonedComponent.Parent) ||
|
||||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
||||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
||||
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
|
||||
{
|
||||
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
||||
return;
|
||||
}
|
||||
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
||||
}
|
||||
|
||||
private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
|
||||
{
|
||||
pod.ConnectedConsole = null;
|
||||
}
|
||||
|
||||
private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (component.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(component.ConnectedConsole, out var console))
|
||||
return;
|
||||
|
||||
if (args.Anchored)
|
||||
{
|
||||
_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
|
||||
return;
|
||||
}
|
||||
_cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
|
||||
return;
|
||||
|
||||
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
|
||||
}
|
||||
// Nyano: Adds float karmaBonus
|
||||
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1, float karmaBonus = 0.25f)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return false;
|
||||
|
||||
// DeltaV - This method should use Entity<CloningPodComponent> pod instead
|
||||
// But I don't want to completely mangle it so we do this here
|
||||
var podEnt = new Entity<CloningPodComponent>(uid, clonePod);
|
||||
|
||||
if (HasComp<ActiveCloningPodComponent>(uid))
|
||||
return false;
|
||||
|
||||
var mind = mindEnt.Comp;
|
||||
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
||||
{
|
||||
if (EntityManager.EntityExists(clone) &&
|
||||
!_mobStateSystem.IsDead(clone) &&
|
||||
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
||||
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
||||
return false; // Mind already has clone
|
||||
|
||||
ClonesWaitingForMind.Remove(mind);
|
||||
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
||||
return false; // Body controlled by mind is not dead
|
||||
|
||||
// Yes, we still need to track down the client because we need to open the Eui
|
||||
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
|
||||
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
|
||||
|
||||
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
|
||||
return false; // whatever body was to be cloned, was not a humanoid
|
||||
|
||||
if (HasComp<SiliconComponent>(bodyToClone))
|
||||
return false; // Goobstation: Don't clone IPCs.
|
||||
|
||||
// Begin Nyano-code: allow paradox anomalies to be cloned.
|
||||
var pref = humanoid.LastProfileLoaded;
|
||||
|
||||
if (pref == null)
|
||||
return false;
|
||||
// End Nyano-code
|
||||
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
||||
return false;
|
||||
|
||||
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
|
||||
return false;
|
||||
|
||||
var cloningCost = (int) Math.Round(physics.FixturesMass);
|
||||
|
||||
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
|
||||
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
|
||||
|
||||
// Check if they have the uncloneable trait
|
||||
if (TryComp<UncloneableComponent>(bodyToClone, out _))
|
||||
if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
|
||||
{
|
||||
if (clonePod.ConnectedConsole != null)
|
||||
{
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value,
|
||||
Loc.GetString("cloning-console-uncloneable-trait-error"),
|
||||
InGameICChatType.Speak, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// biomass checks
|
||||
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
|
||||
|
||||
if (biomassAmount < cloningCost)
|
||||
{
|
||||
if (clonePod.ConnectedConsole != null)
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||
clonePod.UsedBiomass = cloningCost;
|
||||
// end of biomass checks
|
||||
|
||||
// genetic damage checks
|
||||
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
|
||||
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
|
||||
{
|
||||
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
|
||||
chance *= failChanceModifier;
|
||||
|
||||
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
|
||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
|
||||
|
||||
if (_robustRandom.Prob(chance))
|
||||
{
|
||||
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
||||
clonePod.FailedClone = true;
|
||||
AddComp<ActiveCloningPodComponent>(uid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// end of genetic damage checks
|
||||
|
||||
// DeltaV - Replaces CloneAppearance with Metem/Clone via FetchAndSpawnMob
|
||||
var mob = FetchAndSpawnMob(podEnt, pref, speciesPrototype, humanoid, bodyToClone, karmaBonus);
|
||||
|
||||
// Nyano - Summary: adds the potential psionic trait to the reanimated mob.
|
||||
EnsureComp<PotentialPsionicComponent>(mob);
|
||||
|
||||
var ev = new CloningEvent(bodyToClone, mob);
|
||||
RaiseLocalEvent(bodyToClone, ref ev);
|
||||
|
||||
if (!ev.NameHandled)
|
||||
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
|
||||
|
||||
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
|
||||
cloneMindReturn.Mind = mind;
|
||||
cloneMindReturn.Parent = uid;
|
||||
_containerSystem.Insert(mob, clonePod.BodyContainer);
|
||||
ClonesWaitingForMind.Add(mind, mob);
|
||||
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
|
||||
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
|
||||
|
||||
AddComp<ActiveCloningPodComponent>(uid);
|
||||
|
||||
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
|
||||
// Add on special job components to the mob.
|
||||
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
|
||||
{
|
||||
foreach (var special in prototype.Special)
|
||||
{
|
||||
if (special is AddComponentSpecial)
|
||||
special.AfterEquip(mob);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
||||
{
|
||||
cloningPod.Status = status;
|
||||
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
||||
while (query.MoveNext(out var uid, out var _, out var cloning))
|
||||
{
|
||||
if (!_powerReceiverSystem.IsPowered(uid))
|
||||
continue;
|
||||
|
||||
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
||||
continue;
|
||||
|
||||
cloning.CloningProgress += frameTime;
|
||||
if (cloning.CloningProgress < cloning.CloningTime)
|
||||
continue;
|
||||
|
||||
if (cloning.FailedClone)
|
||||
EndFailedCloning(uid, cloning);
|
||||
else
|
||||
Eject(uid, cloning);
|
||||
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
|
||||
RemComp(clone.Value, componentRegistration.Type);
|
||||
CopyComp(original, clone.Value, sourceComp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
||||
/// </summary>
|
||||
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
|
||||
var cloningEv = new CloningEvent(settings, clone.Value);
|
||||
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
|
||||
|
||||
// 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);
|
||||
|
||||
var originalName = Name(original);
|
||||
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
|
||||
originalName = nameModComp.BaseName;
|
||||
|
||||
// This will properly set the BaseName and EntityName for the clone.
|
||||
// Adding the component first before renaming will make sure RefreshNameModifers is called.
|
||||
// Without this the name would get reverted to Urist.
|
||||
// If the clone has no name modifiers, NameModifierComponent will be removed again.
|
||||
EnsureComp<NameModifierComponent>(clone.Value);
|
||||
_metaData.SetEntityName(clone.Value, originalName);
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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!
|
||||
/// </summary>
|
||||
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
|
||||
return;
|
||||
// Iterate over all inventory slots
|
||||
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
|
||||
while (slotEnumerator.NextItem(out var item, out var slot))
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
// Spawn a copy of the item using the original prototype.
|
||||
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
|
||||
// we use a whitelist and blacklist to be sure to exclude any problematic entities
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
|
||||
continue;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
|
||||
{
|
||||
if (!Resolve(uid, ref clonePod))
|
||||
return;
|
||||
|
||||
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
|
||||
return;
|
||||
|
||||
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
|
||||
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
||||
clonePod.CloningProgress = 0f;
|
||||
clonePod.UsedBiomass = 0;
|
||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||
}
|
||||
|
||||
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
|
||||
{
|
||||
clonePod.FailedClone = false;
|
||||
clonePod.CloningProgress = 0f;
|
||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||
var transform = Transform(uid);
|
||||
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
|
||||
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
_audio.PlayPvs(clonePod.ScreamSound, uid);
|
||||
Spawn(clonePod.MobSpawnId, transform.Coordinates);
|
||||
}
|
||||
|
||||
Solution bloodSolution = new();
|
||||
|
||||
var i = 0;
|
||||
while (i < 1)
|
||||
{
|
||||
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
|
||||
bloodSolution.AddReagent("Blood", 50);
|
||||
if (_robustRandom.Prob(0.2f))
|
||||
i++;
|
||||
}
|
||||
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
|
||||
|
||||
if (!_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||
}
|
||||
|
||||
clonePod.UsedBiomass = 0;
|
||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||
}
|
||||
|
||||
public void Reset(RoundRestartCleanupEvent ev)
|
||||
{
|
||||
ClonesWaitingForMind.Clear();
|
||||
var prototype = MetaData(item).EntityPrototype;
|
||||
if (prototype != null)
|
||||
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
using Content.Shared.Cloning;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Cloning.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is added to a marker entity in order to spawn a clone of a random player.
|
||||
/// </summary>
|
||||
[RegisterComponent, EntityCategory("Spawner")]
|
||||
public sealed partial class RandomCloneSpawnerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Cloning settings to be used.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using Content.Server.Cloning.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Cloning;
|
||||
|
||||
/// <summary>
|
||||
/// This deals with spawning and setting up a clone of a random crew member.
|
||||
/// </summary>
|
||||
public sealed class RandomCloneSpawnerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
QueueDel(ent.Owner);
|
||||
|
||||
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
|
||||
{
|
||||
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
|
||||
return;
|
||||
}
|
||||
|
||||
var allHumans = _mind.GetAliveHumans();
|
||||
|
||||
if (allHumans.Count == 0)
|
||||
return;
|
||||
|
||||
var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
|
||||
|
||||
if (bodyToClone != null)
|
||||
_cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@ namespace Content.Server.Explosion.EntitySystems
|
|||
_adminLogger.Add(LogType.Trigger, LogImpact.High,
|
||||
$"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
|
||||
Trigger(ent, args.Source);
|
||||
|
||||
var voice = new VoiceTriggeredEvent(args.Source, message);
|
||||
RaiseLocalEvent(ent, ref voice);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,3 +140,12 @@ namespace Content.Server.Explosion.EntitySystems
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a voice trigger is activated, containing the message that triggered it.
|
||||
/// </summary>
|
||||
/// <param name="Source"> The EntityUid of the entity sending the message</param>
|
||||
/// <param name="Message"> The contents of the message</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct VoiceTriggeredEvent(EntityUid Source, string? Message);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics.Components;
|
||||
|
|
@ -33,9 +34,10 @@ namespace Content.Server.Forensics
|
|||
{
|
||||
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
|
||||
SubscribeLocalEvent<FiberComponent, ContactInteractionEvent>(OnFiberInteract); // DeltaV
|
||||
SubscribeLocalEvent<FiberComponent, MapInitEvent>(OnFiberInit); // DeltaV #1455 - unique glove fibers
|
||||
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
|
||||
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
|
||||
SubscribeLocalEvent<FiberComponent, MapInitEvent>(OnFiberInit, after: [typeof(BloodstreamSystem)]); // DeltaV #1455 - unique glove fibers
|
||||
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
|
||||
// The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
|
||||
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
|
||||
|
||||
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
|
||||
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
|
||||
|
|
@ -81,18 +83,20 @@ namespace Content.Server.Forensics
|
|||
|
||||
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.Fingerprint = GenerateFingerprint();
|
||||
Dirty(ent);
|
||||
if (ent.Comp.Fingerprint == null)
|
||||
RandomizeFingerprint((ent.Owner, ent.Comp));
|
||||
}
|
||||
|
||||
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
|
||||
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
if (component.DNA == String.Empty)
|
||||
Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||
if (ent.Comp.DNA == null)
|
||||
RandomizeDNA((ent.Owner, ent.Comp));
|
||||
else
|
||||
{
|
||||
component.DNA = GenerateDNA();
|
||||
|
||||
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
// If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
|
||||
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +104,7 @@ namespace Content.Server.Forensics
|
|||
{
|
||||
string dna = Loc.GetString("forensics-dna-unknown");
|
||||
|
||||
if (TryComp(uid, out DnaComponent? dnaComp))
|
||||
if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
|
||||
dna = dnaComp.DNA;
|
||||
|
||||
foreach (EntityUid part in args.GibbedParts)
|
||||
|
|
@ -119,7 +123,7 @@ namespace Content.Server.Forensics
|
|||
{
|
||||
foreach (EntityUid hitEntity in args.HitEntities)
|
||||
{
|
||||
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
|
||||
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
|
||||
component.DNAs.Add(hitEntityComp.DNA);
|
||||
}
|
||||
}
|
||||
|
|
@ -331,6 +335,9 @@ namespace Content.Server.Forensics
|
|||
|
||||
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
|
||||
{
|
||||
if (component.DNA == null)
|
||||
return;
|
||||
|
||||
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
|
||||
recipientComp.DNAs.Add(component.DNA);
|
||||
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
|
||||
|
|
@ -338,6 +345,36 @@ namespace Content.Server.Forensics
|
|||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
|
||||
/// Does nothing if it does not have the DnaComponent.
|
||||
/// </summary>
|
||||
public void RandomizeDNA(Entity<DnaComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
ent.Comp.DNA = GenerateDNA();
|
||||
Dirty(ent);
|
||||
|
||||
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Give the entity a new, random fingerprint string.
|
||||
/// Does nothing if it does not have the FingerprintComponent.
|
||||
/// </summary>
|
||||
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
ent.Comp.Fingerprint = GenerateFingerprint();
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer DNA from one entity onto the forensics of another
|
||||
/// </summary>
|
||||
|
|
@ -346,7 +383,7 @@ namespace Content.Server.Forensics
|
|||
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
|
||||
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
|
||||
{
|
||||
if (TryComp<DnaComponent>(donor, out var donorComp))
|
||||
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
|
||||
{
|
||||
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
|
||||
recipientComp.DNAs.Add(donorComp.DNA);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ namespace Content.Server.Ghost
|
|||
mind = _entities.GetComponent<MindComponent>(mindId);
|
||||
}
|
||||
|
||||
if (!_entities.System<GhostSystem>().OnGhostAttempt(mindId, true, true, mind))
|
||||
if (!_entities.System<GhostSystem>().OnGhostAttempt(mindId, true, true, mind: mind))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("ghost-command-denied"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ namespace Content.Server.Ghost
|
|||
return ghost;
|
||||
}
|
||||
|
||||
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null)
|
||||
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, bool forced = false, MindComponent? mind = null)
|
||||
{
|
||||
if (!Resolve(mindId, ref mind))
|
||||
return false;
|
||||
|
|
@ -507,7 +507,12 @@ namespace Content.Server.Ghost
|
|||
var playerEntity = mind.CurrentEntity;
|
||||
|
||||
if (playerEntity != null && viaCommand)
|
||||
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
|
||||
{
|
||||
if (forced)
|
||||
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} was forced to ghost via command");
|
||||
else
|
||||
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
|
||||
}
|
||||
|
||||
var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
|
||||
RaiseLocalEvent(handleEv);
|
||||
|
|
@ -516,7 +521,7 @@ namespace Content.Server.Ghost
|
|||
if (handleEv.Handled)
|
||||
return handleEv.Result;
|
||||
|
||||
if (mind.PreventGhosting)
|
||||
if (mind.PreventGhosting && !forced)
|
||||
{
|
||||
if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
|
||||
{
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ public sealed partial class GhostRoleSystem : EntitySystem // Converted to parti
|
|||
if (player.AttachedEntity == null || !HasComp<GhostComponent>(player.AttachedEntity))
|
||||
return false;
|
||||
|
||||
if (Takeover(player, identifier))
|
||||
if (TryTakeover(player, identifier)) // DeltaV - prevent taking ghost roles in lobby
|
||||
{
|
||||
// takeover successful, we have a winner! remove the winner from other raffles they might be in
|
||||
LeaveAllRaffles(player);
|
||||
|
|
|
|||
|
|
@ -216,18 +216,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
|||
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
||||
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
|
||||
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
|
||||
if (TryComp<DnaComponent>(ent, out var dna))
|
||||
{
|
||||
dna.DNA = _forensicsSystem.GenerateDNA();
|
||||
|
||||
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
}
|
||||
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
|
||||
{
|
||||
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
|
||||
}
|
||||
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
|
||||
// If the entity has the respecive components, then scramble the dna and fingerprint strings
|
||||
_forensicsSystem.RandomizeDNA(ent);
|
||||
_forensicsSystem.RandomizeFingerprint(ent);
|
||||
|
||||
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
|
||||
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
|
||||
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Content.Shared;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Light.EntitySystems;
|
||||
|
|
@ -15,8 +16,7 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
|
|||
|
||||
if (ent.Comp.InitialOffset)
|
||||
{
|
||||
ent.Comp.Offset = _random.Next(ent.Comp.Duration);
|
||||
Dirty(ent);
|
||||
SetOffset(ent, _random.Next(ent.Comp.Duration));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
using Content.Shared.Light.EntitySystems;
|
||||
|
||||
namespace Content.Server.Light.EntitySystems;
|
||||
|
||||
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -15,8 +15,5 @@ namespace Content.Server.Medical.Components
|
|||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float CloningFailChanceMultiplier = 1f;
|
||||
|
||||
// Nyano, needed for Metem Machine.
|
||||
public float MetemKarmaBonus = 0.25f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component
|
|||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool TargetInLOS = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, only opaque objects will block line of sight.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public bool UseOpaqueForLOSChecks = false;
|
||||
|
||||
/// <summary>
|
||||
/// Delay after target is in LOS before we start shooting.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public sealed partial class HTNComponent : NPCComponent
|
|||
/// The base task to use for planning
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite),
|
||||
DataField("rootTask", required: true)]
|
||||
DataField("rootTask", required: true)]
|
||||
public HTNCompoundTask RootTask = default!;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent
|
|||
/// Is this NPC currently planning?
|
||||
/// </summary>
|
||||
[ViewVariables] public bool Planning => PlanningJob != null;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether plans should be made / updated for this entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Enabled = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem
|
|||
component.PlanningJob = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable / disable the hierarchical task network of an entity
|
||||
/// </summary>
|
||||
/// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
|
||||
/// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
|
||||
/// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
[PublicAPI]
|
||||
public void SetHTNEnabled(Entity<HTNComponent> ent, bool state, float planCooldown = 0f)
|
||||
{
|
||||
if (ent.Comp.Enabled == state)
|
||||
return;
|
||||
|
||||
ent.Comp.Enabled = state;
|
||||
ent.Comp.PlanAccumulator = planCooldown;
|
||||
|
||||
ent.Comp.PlanningToken?.Cancel();
|
||||
ent.Comp.PlanningToken = null;
|
||||
|
||||
if (ent.Comp.Plan != null)
|
||||
{
|
||||
var currentOperator = ent.Comp.Plan.CurrentOperator;
|
||||
|
||||
ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
|
||||
ShutdownPlan(ent.Comp);
|
||||
|
||||
ent.Comp.Plan = null;
|
||||
}
|
||||
|
||||
if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
|
||||
RequestPlan(ent.Comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the NPC to replan.
|
||||
/// </summary>
|
||||
|
|
@ -147,12 +180,15 @@ public sealed class HTNSystem : EntitySystem
|
|||
_planQueue.Process();
|
||||
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
|
||||
|
||||
while(query.MoveNext(out var uid, out _, out var comp))
|
||||
while (query.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
// If we're over our max count or it's not MapInit then ignore the NPC.
|
||||
if (count >= maxUpdates)
|
||||
break;
|
||||
|
||||
if (!comp.Enabled)
|
||||
continue;
|
||||
|
||||
if (comp.PlanningJob != null)
|
||||
{
|
||||
if (comp.PlanningJob.Exception != null)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Server.Interaction;
|
||||
using Content.Shared.Physics;
|
||||
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
|
|
@ -13,6 +14,9 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
|
|||
[DataField("rangeKey")]
|
||||
public string RangeKey = "RangeKey";
|
||||
|
||||
[DataField("opaqueKey")]
|
||||
public bool UseOpaqueForLOSChecksKey = true;
|
||||
|
||||
public override void Initialize(IEntitySystemManager sysManager)
|
||||
{
|
||||
base.Initialize(sysManager);
|
||||
|
|
@ -27,7 +31,8 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
|
|||
return false;
|
||||
|
||||
var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
|
||||
var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
|
||||
|
||||
return _interaction.InRangeUnobstructed(owner, target, range);
|
||||
return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
|
|||
[DataField("requireLOS")]
|
||||
public bool RequireLOS = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, only opaque objects will block line of sight.
|
||||
/// </summary>
|
||||
[DataField("opaqueKey")]
|
||||
public bool UseOpaqueForLOSChecks = false;
|
||||
|
||||
// Like movement we add a component and pass it off to the dedicated system.
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
|
|
@ -56,8 +62,10 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
|
|||
public override void Startup(NPCBlackboard blackboard)
|
||||
{
|
||||
base.Startup(blackboard);
|
||||
|
||||
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
|
||||
ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
|
||||
|
||||
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
namespace Content.Server.NPC.Queries.Considerations;
|
||||
|
||||
/// <summary>
|
||||
/// Returns 0f if the NPC has a <see cref="TurretTargetSettingsComponent"/> and the
|
||||
/// target entity is exempt from being targeted, otherwise it returns 1f.
|
||||
/// See <see cref="TurretTargetSettingsSystem.EntityIsTargetForTurret"/>
|
||||
/// for further details on turret target validation.
|
||||
/// </summary>
|
||||
public sealed partial class TurretTargetingCon : UtilityConsideration
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Robust.Shared.Map;
|
||||
|
|
@ -132,8 +133,10 @@ public sealed partial class NPCCombatSystem
|
|||
if (comp.LOSAccumulator < 0f)
|
||||
{
|
||||
comp.LOSAccumulator += UnoccludedCooldown;
|
||||
|
||||
// For consistency with NPC steering.
|
||||
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f);
|
||||
var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
|
||||
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
|
||||
}
|
||||
|
||||
if (!comp.TargetInLOS)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ using Content.Shared.NPC.Systems;
|
|||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Turrets;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
|
|
@ -53,6 +54,7 @@ public sealed class NPCUtilitySystem : EntitySystem
|
|||
[Dependency] private readonly ExamineSystemShared _examine = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
|
||||
[Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
|
||||
|
||||
private EntityQuery<PuddleComponent> _puddleQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
|
@ -358,6 +360,14 @@ public sealed class NPCUtilitySystem : EntitySystem
|
|||
return 1f;
|
||||
return 0f;
|
||||
}
|
||||
case TurretTargetingCon:
|
||||
{
|
||||
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
|
||||
_turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
|
||||
return 1f;
|
||||
|
||||
return 0f;
|
||||
}
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Content.Server.GameTicking;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared._DV.CustomObjectiveSummary; // DeltaV
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mind;
|
||||
|
|
@ -12,10 +13,11 @@ using Robust.Shared.Random;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Objectives.Commands;
|
||||
using Content.Shared._DV.CustomObjectiveSummary; // DeltaV
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Objectives;
|
||||
|
|
@ -28,15 +30,20 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly SharedJobSystem _job = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private IEnumerable<string>? _objectives;
|
||||
|
||||
private bool _showGreentext;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||
|
||||
Subs.CVar(_cfg, CCVars.GameShowGreentext, value => _showGreentext = value, true);
|
||||
|
||||
_prototypeManager.PrototypesReloaded += CreateCompletions;
|
||||
}
|
||||
|
||||
|
|
@ -163,8 +170,11 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||
totalObjectives++;
|
||||
|
||||
agentSummary.Append("- ");
|
||||
/* Begin DeltaV removal - Removed greentext
|
||||
if (progress > 0.99f)
|
||||
if (!_showGreentext)
|
||||
{
|
||||
agentSummary.AppendLine(objectiveTitle);
|
||||
}
|
||||
else if (progress > 0.99f)
|
||||
{
|
||||
agentSummary.AppendLine(Loc.GetString(
|
||||
"objectives-objective-success",
|
||||
|
|
@ -182,13 +192,6 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||
("markupColor", "red")
|
||||
));
|
||||
}
|
||||
End DeltaV removal */
|
||||
// Begin DeltaV Additions - Generic objective
|
||||
agentSummary.AppendLine(Loc.GetString(
|
||||
"objectives-objective",
|
||||
("objective", objectiveTitle)
|
||||
));
|
||||
// End DeltaV Additions
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
|||
|
||||
EnsureComp<LightCycleComponent>(mapUid);
|
||||
|
||||
EnsureComp<SunShadowComponent>(mapUid);
|
||||
EnsureComp<SunShadowCycleComponent>(mapUid);
|
||||
|
||||
var moles = new float[Atmospherics.AdjustedNumberOfGases];
|
||||
moles[(int) Gas.Oxygen] = 21.824779f;
|
||||
moles[(int) Gas.Nitrogen] = 82.10312f;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,20 @@ namespace Content.Server.Singularity.Components
|
|||
[RegisterComponent]
|
||||
public sealed partial class SinguloFoodComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("energy")]
|
||||
public float Energy { get; set; } = 1f;
|
||||
/// <summary>
|
||||
/// Flat adjustment to the singularity's energy when this entity is eaten by the event horizon.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Energy = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier applied to singularity's energy.
|
||||
/// 1.0 = no change, 0.97 = 3% reduction, 1.05 = 5% increase
|
||||
/// </summary>
|
||||
/// /// <remarks>
|
||||
/// This is calculated using the singularity's energy level before <see cref="Energy"/> has been added.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public float EnergyFactor = 1f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,7 +306,12 @@ public sealed class SingularitySystem : SharedSingularitySystem
|
|||
public void OnConsumed(EntityUid uid, SinguloFoodComponent comp, ref EventHorizonConsumedEntityEvent args)
|
||||
{
|
||||
if (EntityManager.TryGetComponent<SingularityComponent>(args.EventHorizonUid, out var singulo))
|
||||
AdjustEnergy(args.EventHorizonUid, comp.Energy, singularity: singulo);
|
||||
{
|
||||
// Calculate the percentage change (positive or negative)
|
||||
var percentageChange = singulo.Energy * (comp.EnergyFactor - 1f);
|
||||
// Apply both the flat and percentage changes
|
||||
AdjustEnergy(args.EventHorizonUid, comp.Energy + percentageChange, singularity: singulo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
|
|||
SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Turn this into a relay event.
|
||||
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
// does the user already has this accent?
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
|
@ -117,7 +118,7 @@ public sealed partial class StoreSystem
|
|||
|
||||
if (listing.Conditions != null)
|
||||
{
|
||||
var args = new ListingConditionArgs(buyer, storeEntity, listing, EntityManager);
|
||||
var args = new ListingConditionArgs(GetBuyerMind(buyer), storeEntity, listing, EntityManager);
|
||||
var conditionsMet = true;
|
||||
|
||||
foreach (var condition in listing.Conditions)
|
||||
|
|
@ -137,6 +138,19 @@ public sealed partial class StoreSystem
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the entity's mind entity, if it has one, to be used for listing conditions.
|
||||
/// If it doesn't have one, or is a mind entity already, it returns itself.
|
||||
/// </summary>
|
||||
/// <param name="buyer">The buying entity.</param>
|
||||
public EntityUid GetBuyerMind(EntityUid buyer)
|
||||
{
|
||||
if (!HasComp<MindComponent>(buyer) && _mind.TryGetMind(buyer, out var buyerMind, out var _))
|
||||
return buyerMind;
|
||||
|
||||
return buyer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a listing appears in a list of given categories
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ public sealed partial class StoreSystem
|
|||
//condition checking because why not
|
||||
if (listing.Conditions != null)
|
||||
{
|
||||
var args = new ListingConditionArgs(component.AccountOwner ?? buyer, uid, listing, EntityManager);
|
||||
var args = new ListingConditionArgs(component.AccountOwner ?? GetBuyerMind(buyer), uid, listing, EntityManager);
|
||||
var conditionsMet = listing.Conditions.All(condition => condition.Condition(args));
|
||||
|
||||
if (!conditionsMet)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
|
||||
namespace Content.Server.Traits.Assorted;
|
||||
|
||||
public sealed class UnrevivableSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UnrevivableComponent, CloningAttemptEvent>(OnCloningAttempt);
|
||||
}
|
||||
|
||||
private void OnCloningAttempt(Entity<UnrevivableComponent> ent, ref CloningAttemptEvent args)
|
||||
{
|
||||
if (!ent.Comp.Cloneable)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,12 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.Emp;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Emp;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
|
|
@ -21,7 +14,6 @@ using Content.Shared.Throwing;
|
|||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Content.Shared.Wall;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
|
@ -32,14 +24,9 @@ namespace Content.Server.VendingMachines
|
|||
public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly PricingSystem _pricing = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _light = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
private const float WallVendEjectDistanceFromWall = 1f;
|
||||
|
||||
|
|
@ -55,11 +42,6 @@ namespace Content.Server.VendingMachines
|
|||
|
||||
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
|
||||
|
||||
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
|
||||
{
|
||||
subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
|
||||
});
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
|
||||
|
|
@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines
|
|||
|
||||
if (HasComp<ApcPowerReceiverComponent>(uid))
|
||||
{
|
||||
TryUpdateVisualState(uid, component);
|
||||
TryUpdateVisualState((uid, component));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,26 +83,15 @@ namespace Content.Server.VendingMachines
|
|||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
if (args.Actor is not { Valid: true } entity || Deleted(entity))
|
||||
return;
|
||||
|
||||
AuthorizedVend(uid, entity, args.Type, args.ID, component);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
TryUpdateVisualState(uid, component);
|
||||
TryUpdateVisualState((uid, component));
|
||||
}
|
||||
|
||||
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
|
||||
{
|
||||
vendComponent.Broken = true;
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
}
|
||||
|
||||
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
|
||||
|
|
@ -128,7 +99,7 @@ namespace Content.Server.VendingMachines
|
|||
if (!args.DamageIncreased && component.Broken)
|
||||
{
|
||||
component.Broken = false;
|
||||
TryUpdateVisualState(uid, component);
|
||||
TryUpdateVisualState((uid, component));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +110,11 @@ namespace Content.Server.VendingMachines
|
|||
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
|
||||
_random.Prob(component.DispenseOnHitChance.Value))
|
||||
{
|
||||
if (component.DispenseOnHitCooldown > 0f)
|
||||
component.DispenseOnHitCoolingDown = true;
|
||||
if (component.DispenseOnHitCooldown != null)
|
||||
{
|
||||
component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
|
||||
}
|
||||
|
||||
EjectRandom(uid, throwItem: true, forceEject: true, component);
|
||||
}
|
||||
}
|
||||
|
|
@ -199,145 +173,6 @@ namespace Content.Server.VendingMachines
|
|||
Dirty(uid, component);
|
||||
}
|
||||
|
||||
public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
if (vendComponent.Denying)
|
||||
return;
|
||||
|
||||
vendComponent.Denying = true;
|
||||
Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f));
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user is authorized to use this vending machine
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="sender">Entity trying to use the vending machine</param>
|
||||
/// <param name="vendComponent"></param>
|
||||
public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return false;
|
||||
|
||||
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
|
||||
return true;
|
||||
|
||||
if (_accessReader.IsAllowed(sender, uid, accessReader))
|
||||
return true;
|
||||
|
||||
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
|
||||
Deny(uid, vendComponent);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
|
||||
/// or the item doesn't exist in its inventory.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="type">The type of inventory the item is from</param>
|
||||
/// <param name="itemId">The prototype ID of the item</param>
|
||||
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
|
||||
/// <param name="vendComponent"></param>
|
||||
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = GetEntry(uid, itemId, type, vendComponent);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
|
||||
Deny(uid, vendComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.Amount <= 0)
|
||||
{
|
||||
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
|
||||
Deny(uid, vendComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(entry.ID))
|
||||
return;
|
||||
|
||||
|
||||
// Start Ejecting, and prevent users from ordering while anim playing
|
||||
vendComponent.Ejecting = true;
|
||||
vendComponent.NextItemToEject = entry.ID;
|
||||
vendComponent.ThrowNextItem = throwItem;
|
||||
|
||||
if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
|
||||
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
|
||||
|
||||
entry.Amount--;
|
||||
Dirty(uid, vendComponent);
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
Audio.PlayPvs(vendComponent.SoundVend, uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="sender">Entity that is trying to use the vending machine</param>
|
||||
/// <param name="type">The type of inventory the item is from</param>
|
||||
/// <param name="itemId">The prototype ID of the item</param>
|
||||
/// <param name="component"></param>
|
||||
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
|
||||
{
|
||||
if (IsAuthorized(uid, sender, component))
|
||||
{
|
||||
TryEjectVendorItem(uid, type, itemId, component.CanShoot, component);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to update the visuals of the component based on its current state.
|
||||
/// </summary>
|
||||
public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
var finalState = VendingMachineVisualState.Normal;
|
||||
if (vendComponent.Broken)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Broken;
|
||||
}
|
||||
else if (vendComponent.Ejecting)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Eject;
|
||||
}
|
||||
else if (vendComponent.Denying)
|
||||
{
|
||||
finalState = VendingMachineVisualState.Deny;
|
||||
}
|
||||
else if (!this.IsPowered(uid, EntityManager))
|
||||
{
|
||||
finalState = VendingMachineVisualState.Off;
|
||||
}
|
||||
|
||||
if (_light.TryGetLight(uid, out var pointlight))
|
||||
{
|
||||
var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
|
||||
_light.SetEnabled(uid, lightState, pointlight);
|
||||
}
|
||||
|
||||
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
|
||||
/// </summary>
|
||||
|
|
@ -367,18 +202,18 @@ namespace Content.Server.VendingMachines
|
|||
}
|
||||
else
|
||||
{
|
||||
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
|
||||
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
|
||||
protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
// No need to update the visual state because we never changed it during a forced eject
|
||||
if (!forceEject)
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
|
||||
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
|
||||
{
|
||||
|
|
@ -411,68 +246,17 @@ namespace Content.Server.VendingMachines
|
|||
vendComponent.ThrowNextItem = false;
|
||||
}
|
||||
|
||||
private VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return null;
|
||||
|
||||
if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return component.EmaggedInventory.GetValueOrDefault(entryId);
|
||||
|
||||
if (type == InventoryType.Contraband && component.Contraband)
|
||||
return component.ContrabandInventory.GetValueOrDefault(entryId);
|
||||
|
||||
return component.Inventory.GetValueOrDefault(entryId);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<VendingMachineComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (comp.Ejecting)
|
||||
{
|
||||
comp.EjectAccumulator += frameTime;
|
||||
if (comp.EjectAccumulator >= comp.EjectDelay)
|
||||
{
|
||||
comp.EjectAccumulator = 0f;
|
||||
comp.Ejecting = false;
|
||||
|
||||
EjectItem(uid, comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.Denying)
|
||||
{
|
||||
comp.DenyAccumulator += frameTime;
|
||||
if (comp.DenyAccumulator >= comp.DenyDelay)
|
||||
{
|
||||
comp.DenyAccumulator = 0f;
|
||||
comp.Denying = false;
|
||||
|
||||
TryUpdateVisualState(uid, comp);
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.DispenseOnHitCoolingDown)
|
||||
{
|
||||
comp.DispenseOnHitAccumulator += frameTime;
|
||||
if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
|
||||
{
|
||||
comp.DispenseOnHitAccumulator = 0f;
|
||||
comp.DispenseOnHitCoolingDown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>();
|
||||
while (disabled.MoveNext(out var uid, out _, out var comp))
|
||||
{
|
||||
if (comp.NextEmpEject < _timing.CurTime)
|
||||
{
|
||||
EjectRandom(uid, true, false, comp);
|
||||
comp.NextEmpEject += TimeSpan.FromSeconds(5 * comp.EjectDelay);
|
||||
comp.NextEmpEject += (5 * comp.EjectDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -485,7 +269,7 @@ namespace Content.Server.VendingMachines
|
|||
RestockInventoryFromPrototype(uid, vendComponent);
|
||||
|
||||
Dirty(uid, vendComponent);
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
TryUpdateVisualState((uid, vendComponent));
|
||||
}
|
||||
|
||||
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Server.VoiceTrigger;
|
||||
|
||||
/// <summary>
|
||||
/// Entities with this component, Containers, and TriggerOnVoiceComponent will insert any item or extract the spoken item after the TriggerOnVoiceComponent has been activated
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class StorageVoiceControlComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to determine which slots the component can be used in.
|
||||
/// <remarks>
|
||||
/// If not set, the component can be used anywhere, even while inside other containers.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags? AllowedSlots;
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Server.Containers;
|
||||
|
||||
namespace Content.Server.VoiceTrigger;
|
||||
|
||||
/// <summary>
|
||||
/// Allows storages to be manipulated using voice commands.
|
||||
/// </summary>
|
||||
public sealed class StorageVoiceControlSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly HandsSystem _hands = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly StorageSystem _storage = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<StorageVoiceControlComponent, VoiceTriggeredEvent>(VoiceTriggered);
|
||||
}
|
||||
|
||||
private void VoiceTriggered(Entity<StorageVoiceControlComponent> ent, ref VoiceTriggeredEvent args)
|
||||
{
|
||||
// Check if the component has any slot restrictions via AllowedSlots
|
||||
// If it has slot restrictions, check if the item is in a slot that is allowed
|
||||
if (ent.Comp.AllowedSlots != null && _inventory.TryGetContainingSlot(ent.Owner, out var itemSlot) &&
|
||||
(itemSlot.SlotFlags & ent.Comp.AllowedSlots) == 0)
|
||||
return;
|
||||
|
||||
// Don't do anything if there is no message
|
||||
if (args.Message == null)
|
||||
return;
|
||||
|
||||
// Get the storage component
|
||||
if (!TryComp<StorageComponent>(ent, out var storage))
|
||||
return;
|
||||
|
||||
// Get the hands component
|
||||
if (!TryComp<HandsComponent>(args.Source, out var hands))
|
||||
return;
|
||||
|
||||
// If the player has something in their hands, try to insert it into the storage
|
||||
if (hands.ActiveHand != null && hands.ActiveHand.HeldEntity.HasValue)
|
||||
{
|
||||
// Disallow insertion and provide a reason why if the person decides to insert the item into itself
|
||||
if (ent.Owner.Equals(hands.ActiveHand.HeldEntity.Value))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("comp-storagevoicecontrol-self-insert", ("entity", hands.ActiveHand.HeldEntity.Value)), ent, args.Source);
|
||||
return;
|
||||
}
|
||||
if (_storage.CanInsert(ent, hands.ActiveHand.HeldEntity.Value, out var failedReason))
|
||||
{
|
||||
// We adminlog before insertion, otherwise the logger will attempt to pull info on an entity that no longer is present and throw an exception
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} inserted {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
|
||||
_storage.Insert(ent, hands.ActiveHand.HeldEntity.Value, out _);
|
||||
return;
|
||||
}
|
||||
{
|
||||
// Tell the player the reason why the item couldn't be inserted
|
||||
if (failedReason == null)
|
||||
return;
|
||||
_popup.PopupEntity(Loc.GetString(failedReason), ent, args.Source);
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Low,
|
||||
$"{ToPrettyString(args.Source)} failed to insert {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If otherwise, we're retrieving an item, so check all the items currently in the attached storage
|
||||
foreach (var item in storage.Container.ContainedEntities)
|
||||
{
|
||||
// Get the item's name
|
||||
var itemName = MetaData(item).EntityName;
|
||||
// The message doesn't match the item name the requestor requested, skip and move on to the next item
|
||||
if (!args.Message.Contains(itemName, StringComparison.InvariantCultureIgnoreCase))
|
||||
continue;
|
||||
|
||||
// We found the item we want, so draw it from storage and place it into the player's hands
|
||||
if (storage.Container.ContainedEntities.Count != 0)
|
||||
{
|
||||
_container.RemoveEntity(ent, item);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} retrieved {ToPrettyString(item)} from {ToPrettyString(ent)} via voice control");
|
||||
_hands.TryPickup(args.Source, item, handsComp: hands);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using Content.Server.Actions;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chat;
|
||||
|
|
@ -14,7 +13,7 @@ using Content.Server.NPC.HTN;
|
|||
using Content.Server.NPC.Systems;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Abilities.Psionics;
|
||||
using Content.Shared.Abilities.Psionics; // DeltaV
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
using Content.Shared.Damage;
|
||||
|
|
@ -26,12 +25,12 @@ using Content.Shared.Mobs;
|
|||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.NPC.Components; // DeltaV
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Nutrition.AnimalHusbandry;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Zombies;
|
||||
using Content.Shared.Prying.Components;
|
||||
|
|
@ -61,8 +60,8 @@ public sealed partial class ZombieSystem
|
|||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
[Dependency] private readonly NPCSystem _npc = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Handles an entity turning into a zombie when they die or go into crit
|
||||
|
|
@ -235,7 +234,7 @@ public sealed partial class ZombieSystem
|
|||
|
||||
_faction.ClearFactions(target, dirty: false);
|
||||
_faction.AddFaction(target, "Zombie");
|
||||
EnsureComp<NoFriendlyFireComponent>(target); // prevent shitters biting other zombies
|
||||
EnsureComp<NoFriendlyFireComponent>(target); // DeltaV - prevent shitters biting other zombies
|
||||
|
||||
//gives it the funny "Zombie ___" name.
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
|
|
@ -252,7 +251,7 @@ public sealed partial class ZombieSystem
|
|||
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
||||
{
|
||||
//Zombie role for player manifest
|
||||
_roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
|
||||
_role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
|
||||
|
||||
//Greeting message for new bebe zombers
|
||||
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
||||
|
|
|
|||
|
|
@ -5,18 +5,20 @@ using Content.Server.Chat;
|
|||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Emoting.Systems;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
|
@ -38,7 +40,7 @@ namespace Content.Server.Zombies
|
|||
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
|
||||
public const SlotFlags ProtectiveSlots =
|
||||
SlotFlags.FEET |
|
||||
|
|
@ -63,6 +65,8 @@ namespace Content.Server.Zombies
|
|||
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
||||
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
||||
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
||||
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
|
||||
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
|
||||
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
||||
|
|
@ -264,7 +268,7 @@ namespace Content.Server.Zombies
|
|||
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
|
||||
/// <param name="zombiecomp"></param>
|
||||
/// <remarks>
|
||||
/// this currently only restore the name and skin/eye color from before zombified
|
||||
/// this currently only restore the skin/eye color from before zombified
|
||||
/// TODO: completely rethink how zombies are done to allow reversal.
|
||||
/// </remarks>
|
||||
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
|
||||
|
|
@ -284,14 +288,25 @@ namespace Content.Server.Zombies
|
|||
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
|
||||
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
|
||||
|
||||
_nameMod.RefreshNameModifiers(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args)
|
||||
private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
|
||||
{
|
||||
if (UnZombify(args.Source, args.Target, zombiecomp))
|
||||
args.NameHandled = true;
|
||||
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
|
||||
}
|
||||
|
||||
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
|
||||
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
|
||||
{
|
||||
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
|
||||
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
|
||||
}
|
||||
|
||||
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
|
||||
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
|
||||
{
|
||||
_role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Content.Server.Bible.Components;
|
||||
using Content.Server._DV.Cloning;
|
||||
using Content.Shared.Abilities.Psionics;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
|
|
@ -118,7 +117,7 @@ public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem
|
|||
return;
|
||||
}
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target) && !HasComp<MetempsychosisKarmaComponent>(target))
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("altar-failure-reason-target-humanoid", ("target", target)), ent, user, PopupType.SmallCaution);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,172 +0,0 @@
|
|||
using Content.Server._DV.Cloning;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.SSDIndicator;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.Psionics;
|
||||
using Robust.Shared.Random;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
|
||||
namespace Content.Server.Cloning;
|
||||
|
||||
public sealed partial class CloningSystem
|
||||
{
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly GrammarSystem _grammar = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity prototype to spawn for a clone based on karma and chance calculations.
|
||||
/// </summary>
|
||||
private string GetSpawnEntity(Entity<MetempsychoticMachineComponent> ent, float karmaBonus, SpeciesPrototype oldSpecies, out SpeciesPrototype? species, int karma = 0)
|
||||
{
|
||||
// First time being cloned - return original species
|
||||
if (karma == 0)
|
||||
{
|
||||
species = oldSpecies;
|
||||
return oldSpecies.Prototype;
|
||||
}
|
||||
|
||||
var chance = ent.Comp.HumanoidBaseChance + karmaBonus;
|
||||
chance -= (1 - ent.Comp.HumanoidBaseChance) * karma;
|
||||
|
||||
// Perfect clone chance
|
||||
if (chance > 1 && _robustRandom.Prob(chance - 1))
|
||||
{
|
||||
species = oldSpecies;
|
||||
return oldSpecies.Prototype;
|
||||
}
|
||||
|
||||
// Roll for humanoid vs non-humanoid
|
||||
chance = Math.Clamp(chance, 0, 1);
|
||||
if (_robustRandom.Prob(chance))
|
||||
{
|
||||
if (_prototype.TryIndex(ent.Comp.MetempsychoticHumanoidPool, out var humanoidPool))
|
||||
{
|
||||
var protoId = humanoidPool.Pick();
|
||||
if (_prototype.TryIndex<SpeciesPrototype>(protoId, out var speciesPrototype))
|
||||
{
|
||||
species = speciesPrototype;
|
||||
return speciesPrototype.Prototype;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_prototype.TryIndex(ent.Comp.MetempsychoticNonHumanoidPool, out var nonHumanoidPool))
|
||||
{
|
||||
// For non-humanoids, return the entity prototype directly
|
||||
species = null;
|
||||
return nonHumanoidPool.Pick();
|
||||
}
|
||||
|
||||
// Fallback to original species if prototype indexing fails
|
||||
Log.Error("Failed to get valid clone type - falling back to original species");
|
||||
species = oldSpecies;
|
||||
return oldSpecies.Prototype;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles fetching the mob and managing appearance for cloning with metempsychosis mechanics
|
||||
/// </summary>
|
||||
private EntityUid FetchAndSpawnMob(
|
||||
Entity<CloningPodComponent> pod,
|
||||
HumanoidCharacterProfile pref,
|
||||
SpeciesPrototype speciesPrototype,
|
||||
HumanoidAppearanceComponent humanoid,
|
||||
EntityUid bodyToClone,
|
||||
float karmaBonus)
|
||||
{
|
||||
List<Sex> sexes = [];
|
||||
var switchingSpecies = false;
|
||||
var applyKarma = false;
|
||||
var toSpawn = speciesPrototype.Prototype;
|
||||
|
||||
// Get existing karma score or start at 0
|
||||
var karmaScore = 0;
|
||||
if (TryComp<MetempsychosisKarmaComponent>(bodyToClone, out var oldKarma))
|
||||
{
|
||||
karmaScore = oldKarma.Score;
|
||||
}
|
||||
|
||||
if (TryComp<MetempsychoticMachineComponent>(pod.Owner, out var metem))
|
||||
{
|
||||
var metemEntity = new Entity<MetempsychoticMachineComponent>(pod.Owner, metem);
|
||||
toSpawn = GetSpawnEntity(metemEntity, karmaBonus, speciesPrototype, out var newSpecies, karmaScore);
|
||||
applyKarma = true;
|
||||
|
||||
if (newSpecies != null)
|
||||
{
|
||||
sexes = newSpecies.Sexes;
|
||||
speciesPrototype = newSpecies;
|
||||
|
||||
if (speciesPrototype.ID != newSpecies.ID)
|
||||
switchingSpecies = true;
|
||||
}
|
||||
}
|
||||
|
||||
var mob = Spawn(toSpawn, _transformSystem.GetMapCoordinates(pod.Owner));
|
||||
|
||||
// Only try to handle humanoid appearance if we have a humanoid component
|
||||
if (TryComp<HumanoidAppearanceComponent>(mob, out var newHumanoid))
|
||||
{
|
||||
if (switchingSpecies || HasComp<MetempsychosisKarmaComponent>(bodyToClone))
|
||||
{
|
||||
pref = HumanoidCharacterProfile.RandomWithSpecies(newHumanoid.Species);
|
||||
if (sexes.Contains(humanoid.Sex))
|
||||
pref = pref.WithSex(humanoid.Sex);
|
||||
|
||||
pref = pref.WithGender(humanoid.Gender);
|
||||
pref = pref.WithAge(humanoid.Age);
|
||||
}
|
||||
|
||||
_humanoidSystem.LoadProfile(mob, pref);
|
||||
}
|
||||
|
||||
if (applyKarma)
|
||||
{
|
||||
var karma = EnsureComp<MetempsychosisKarmaComponent>(mob);
|
||||
karma.Score = karmaScore + 1; // Increment karma score
|
||||
}
|
||||
|
||||
var ev = new CloningEvent(bodyToClone, mob);
|
||||
RaiseLocalEvent(bodyToClone, ref ev);
|
||||
|
||||
if (!ev.NameHandled)
|
||||
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
|
||||
|
||||
var grammar = EnsureComp<GrammarComponent>(mob);
|
||||
var grammarEnt = new Entity<GrammarComponent>(mob, grammar);
|
||||
_grammar.SetProperNoun(grammarEnt, true);
|
||||
_grammar.SetGender(grammarEnt, humanoid.Gender);
|
||||
Dirty(mob, grammar);
|
||||
|
||||
SetupBasicComponents(mob);
|
||||
|
||||
return mob;
|
||||
}
|
||||
|
||||
// I hate this
|
||||
private void SetupBasicComponents(EntityUid mob)
|
||||
{
|
||||
EnsureComp<PotentialPsionicComponent>(mob);
|
||||
EnsureComp<SpeechComponent>(mob);
|
||||
EnsureComp<DamageForceSayComponent>(mob);
|
||||
EnsureComp<EmotingComponent>(mob);
|
||||
EnsureComp<MindContainerComponent>(mob);
|
||||
EnsureComp<SSDIndicatorComponent>(mob);
|
||||
RemComp<ReplacementAccentComponent>(mob);
|
||||
RemComp<MonkeyAccentComponent>(mob);
|
||||
RemComp<SentienceTargetComponent>(mob);
|
||||
RemComp<GhostTakeoverAvailableComponent>(mob);
|
||||
|
||||
_tag.AddTag(mob, "DoorBumpOpener");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
namespace Content.Server._DV.Cloning;
|
||||
|
||||
/// <summary>
|
||||
/// This tracks how many times you have already been cloned and lowers your chance of getting a humanoid each time.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class MetempsychosisKarmaComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public int Score;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
using Content.Shared.Random;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._DV.Cloning;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class MetempsychoticMachineComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Base probability of remaining humanoid during cloning. Higher karma reduces this chance.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float HumanoidBaseChance = 0.75f;
|
||||
|
||||
/// <summary>
|
||||
/// Species prototypes pool to use for humanoids.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<WeightedRandomPrototype> MetempsychoticHumanoidPool = "MetempsychoticHumanoidPool";
|
||||
|
||||
/// <summary>
|
||||
/// Entitiy prototypes pool to use for non-humanoids.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<WeightedRandomPrototype> MetempsychoticNonHumanoidPool = "MetempsychoticNonhumanoidPool";
|
||||
}
|
||||
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
using Content.Shared.Cloning;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a paradox anomaly of a random person when taken by a player.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ParadoxClonerRuleComponent : Component;
|
||||
public sealed partial class ParadoxClonerRuleComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public ProtoId<CloningSettingsPrototype> CloningSettings = "BaseClone";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using Content.Server.Antag;
|
||||
using Content.Server.Cloning;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Psionics;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Server.Terminator.Systems;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Mind;
|
||||
|
|
@ -25,11 +27,13 @@ namespace Content.Server.StationEvents.Events;
|
|||
/// </summary>
|
||||
public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly PsionicsSystem _psionics = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||
[Dependency] private readonly TerminatorSystem _terminator = default!;
|
||||
|
|
@ -46,30 +50,30 @@ public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComp
|
|||
if (args.Session?.AttachedEntity is not {} spawner)
|
||||
return;
|
||||
|
||||
var settings = _proto.Index(ent.Comp.CloningSettings);
|
||||
Log.Debug($"Rule {ToPrettyString(ent)} creating a paradox anomaly using spawner {spawner}");
|
||||
if (!TrySpawnParadoxAnomaly(spawner, out var clone))
|
||||
if (!TrySpawnParadoxAnomaly(spawner, settings, out var clone))
|
||||
return;
|
||||
|
||||
Log.Info($"Created paradox anomaly {ToPrettyString(clone):clone}");
|
||||
args.Entity = clone;
|
||||
}
|
||||
|
||||
private bool TrySpawnParadoxAnomaly(EntityUid spawner, [NotNullWhen(true)] out EntityUid? clone)
|
||||
private bool TrySpawnParadoxAnomaly(EntityUid spawner, CloningSettingsPrototype settings, [NotNullWhen(true)] out EntityUid? clone)
|
||||
{
|
||||
clone = null;
|
||||
|
||||
// Get a list of potential candidates
|
||||
var candidates = new List<(EntityUid, EntityUid, ProtoId<JobPrototype>, HumanoidCharacterProfile)>();
|
||||
var candidates = new List<(EntityUid, EntityUid, ProtoId<JobPrototype>)>();
|
||||
var query = EntityQueryEnumerator<MindContainerComponent, HumanoidAppearanceComponent>();
|
||||
while (query.MoveNext(out var uid, out var mindContainer, out var humanoid))
|
||||
{
|
||||
if (humanoid.LastProfileLoaded is {} profile &&
|
||||
mindContainer.Mind is {} mindId &&
|
||||
if (mindContainer.Mind is {} mindId &&
|
||||
!_role.MindIsAntagonist(mindId) &&
|
||||
_role.MindHasRole<JobRoleComponent>(mindId, out var role) &&
|
||||
role?.Comp1.JobPrototype is {} job)
|
||||
{
|
||||
candidates.Add((uid, mindId, job, profile));
|
||||
candidates.Add((uid, mindId, job));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,21 +83,31 @@ public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComp
|
|||
return false;
|
||||
}
|
||||
|
||||
clone = SpawnParadoxAnomaly(spawner, candidates);
|
||||
return true;
|
||||
// tries20 my beloved
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
clone = SpawnParadoxAnomaly(spawner, settings, candidates);
|
||||
if (clone != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Error("Failed to clone any eligible player!");
|
||||
return false;
|
||||
}
|
||||
|
||||
private EntityUid SpawnParadoxAnomaly(EntityUid spawner, List<(EntityUid, EntityUid, ProtoId<JobPrototype>, HumanoidCharacterProfile)> candidates)
|
||||
private EntityUid? SpawnParadoxAnomaly(EntityUid spawner, CloningSettingsPrototype settings, List<(EntityUid, EntityUid, ProtoId<JobPrototype>)> candidates)
|
||||
{
|
||||
// Select a candidate.
|
||||
var (uid, mindId, job, profile) = _random.Pick(candidates);
|
||||
var (uid, mindId, job) = _random.Pick(candidates);
|
||||
|
||||
// Spawn the clone.
|
||||
var coords = Transform(spawner).Coordinates;
|
||||
var coords = _transform.GetMapCoordinates(spawner);
|
||||
var station = _station.GetOwningStation(uid);
|
||||
var spawned = _stationSpawning.SpawnPlayerMob(coords, job, profile, station);
|
||||
if (!_cloning.TryCloning(uid, coords, settings, out var mob))
|
||||
return null;
|
||||
|
||||
// Set the kill target to the chosen player
|
||||
var spawned = mob.Value;
|
||||
_terminator.SetTarget(spawned, mindId);
|
||||
|
||||
// guaranteed psionic power
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
using Content.Server.Advertise;
|
||||
using Content.Server.Advertise.Components;
|
||||
using Content.Shared._DV.VendingMachines;
|
||||
|
||||
namespace Content.Server._DV.VendingMachines;
|
||||
|
||||
public sealed class ShopVendorSystem : SharedShopVendorSystem
|
||||
{
|
||||
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
|
@ -38,10 +34,4 @@ public sealed class ShopVendorSystem : SharedShopVendorSystem
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AfterPurchase(Entity<ShopVendorComponent> ent)
|
||||
{
|
||||
if (TryComp<SpeakOnUIClosedComponent>(ent, out var speak))
|
||||
_speakOnUIClosed.TrySetFlag((ent.Owner, speak));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,13 @@ public sealed partial class GhettoSurgerySystem : EntitySystem
|
|||
|
||||
private void OnSharpShutdown(Entity<SharpComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
if (ent.Comp.HadScalpel)
|
||||
if (!ent.Comp.HadSurgeryTool)
|
||||
RemComp<SurgeryToolComponent>(ent);
|
||||
|
||||
if (!ent.Comp.HadScalpel)
|
||||
RemComp<ScalpelComponent>(ent);
|
||||
|
||||
if (ent.Comp.HadBoneSaw)
|
||||
if (!ent.Comp.HadBoneSaw)
|
||||
RemComp<BoneSawComponent>(ent);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,8 @@ public sealed class SurgerySystem : SharedSurgerySystem
|
|||
}
|
||||
}
|
||||
|
||||
dnas.Remove(target.Comp1.DNA);
|
||||
if (target.Comp1.DNA is {} dna)
|
||||
dnas.Remove(dna);
|
||||
|
||||
return total + dnas.Count * target.Comp2.CrossContaminationDirtinessLevel;
|
||||
}
|
||||
|
|
@ -210,8 +211,11 @@ public sealed class SurgerySystem : SharedSurgerySystem
|
|||
Dirty(ent, dirtiness);
|
||||
}
|
||||
|
||||
private void AddDNA(EntityUid ent, string dna)
|
||||
private void AddDNA(EntityUid ent, string? dna)
|
||||
{
|
||||
if (dna == null)
|
||||
return;
|
||||
|
||||
var contamination = EnsureComp<SurgeryCrossContaminationComponent>(ent);
|
||||
contamination.DNAs.Add(dna);
|
||||
}
|
||||
|
|
@ -268,9 +272,11 @@ public sealed class SurgerySystem : SharedSurgerySystem
|
|||
|
||||
private void OnSurgerySpecialDamageChange(Entity<SurgerySpecialDamageChangeEffectComponent> ent, ref SurgeryStepDamageChangeEvent args)
|
||||
{
|
||||
// Begin DeltaV - this shit was killed
|
||||
// Im killing this shit soon too, inshallah.
|
||||
if (ent.Comp.DamageType == "Rot")
|
||||
_rot.ReduceAccumulator(args.Body, TimeSpan.FromSeconds(2147483648)); // BEHOLD, SHITCODE THAT I JUST COPY PASTED. I'll redo it at some point, pinky swear :)
|
||||
// if (ent.Comp.DamageType == "Rot")
|
||||
// _rot.ReduceAccumulator(args.Body, TimeSpan.FromSeconds(2147483648)); // BEHOLD, SHITCODE THAT I JUST COPY PASTED. I'll redo it at some point, pinky swear :)
|
||||
// End DeltaV - this shit was killed
|
||||
/*else if (ent.Comp.DamageType == "Eye"
|
||||
&& TryComp(ent, out BlindableComponent? blindComp)
|
||||
&& blindComp.EyeDamage > 0)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,15 @@ namespace Content.Shared.Access
|
|||
/// <summary>
|
||||
/// The player-visible name of the access level, in the ID card console and such.
|
||||
/// </summary>
|
||||
[DataField("name")]
|
||||
[DataField]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Denotes whether this access level is intended to be assignable to a crew ID card.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool CanAddToIdCard = true;
|
||||
|
||||
public string GetAccessLevelName()
|
||||
{
|
||||
if (Name is { } name)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
using Content.Shared.Advertise.Systems;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Advertise.Components;
|
||||
namespace Content.Shared.Advertise.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally
|
||||
/// requiring that a Flag be set as well.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))]
|
||||
public sealed partial class SpeakOnUIClosedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component
|
|||
/// <summary>
|
||||
/// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Flag;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using SpeakOnUIClosedComponent = Content.Shared.Advertise.Components.SpeakOnUIClosedComponent;
|
||||
|
||||
namespace Content.Shared.Advertise.Systems;
|
||||
|
||||
public abstract class SharedSpeakOnUIClosedSystem : EntitySystem
|
||||
{
|
||||
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return false;
|
||||
|
||||
entity.Comp.Flag = value;
|
||||
Dirty(entity);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -60,5 +60,17 @@ public sealed partial class CCVars
|
|||
public static readonly CVarDef<float> SpeechBubbleBackgroundOpacity =
|
||||
CVarDef.Create("accessibility.speech_bubble_background_opacity", 0.75f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, censors character nudity by forcing clothes markings on characters, selected by the client.
|
||||
/// Both this and AccessibilityServerCensorNudity must be false to display nudity on the client.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AccessibilityClientCensorNudity =
|
||||
CVarDef.Create("accessibility.censor_nudity", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, censors character nudity by forcing clothes markings on characters, selected by the server.
|
||||
/// Both this and AccessibilityClientCensorNudity must be false to display nudity on the client.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> AccessibilityServerCensorNudity =
|
||||
CVarDef.Create("accessibility.server_censor_nudity", false, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ public sealed partial class CCVars
|
|||
public static readonly CVarDef<bool>
|
||||
GameLobbyEnableWin = CVarDef.Create("game.enablewin", true, CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Controls if round-end window shows whether the objective was completed or not.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool>
|
||||
GameShowGreentext = CVarDef.Create("game.showgreentext", true, CVar.ARCHIVE | CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Controls the maximum number of character slots a player is allowed to have.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -36,9 +36,6 @@ namespace Content.Shared.Chemistry.Components
|
|||
[DataField]
|
||||
public string? SolutionName;
|
||||
|
||||
[DataField]
|
||||
public string InitialName = string.Empty;
|
||||
|
||||
[DataField]
|
||||
public string InitialDescription = string.Empty;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
namespace Content.Shared.Cloning.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised before a mob is cloned. Cancel to prevent cloning.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a new mob got spawned when cloning a humanoid.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid);
|
||||
|
|
@ -10,8 +10,8 @@ namespace Content.Shared.Cloning;
|
|||
[RegisterComponent]
|
||||
public sealed partial class CloningPodComponent : Component
|
||||
{
|
||||
[ValidatePrototypeId<SinkPortPrototype>]
|
||||
public const string PodPort = "CloningPodReceiver";
|
||||
[DataField]
|
||||
public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
|
||||
|
||||
[ViewVariables]
|
||||
public ContainerSlot BodyContainer = default!;
|
||||
|
|
@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
|
|||
/// <summary>
|
||||
/// The material that is used to clone entities.
|
||||
/// </summary>
|
||||
[DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
|
||||
|
||||
/// <summary>
|
||||
/// The current amount of time it takes to clone a body
|
||||
/// The current amount of time it takes to clone a body.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float CloningTime = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// The mob to spawn on emag
|
||||
/// The mob to spawn on emag.
|
||||
/// </summary>
|
||||
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public EntProtoId MobSpawnId = "MobAbomination";
|
||||
|
||||
// TODO: Remove this from here when cloning and/or zombies are refactored
|
||||
[DataField("screamSound")]
|
||||
/// <summary>
|
||||
/// The sound played when a mob is spawned from an emagged cloning pod.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(4),
|
||||
|
|
@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
|
|||
Gore,
|
||||
NoMind
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after a new mob got spawned when cloning a humanoid
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public struct CloningEvent
|
||||
{
|
||||
public bool NameHandled = false;
|
||||
|
||||
public readonly EntityUid Source;
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public CloningEvent(EntityUid source, EntityUid target)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
|
||||
|
||||
namespace Content.Shared.Cloning;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for cloning a humanoid.
|
||||
/// Used to decide which components should be copied.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
[ParentDataField(typeof(PrototypeIdArraySerializer<CloningSettingsPrototype>))]
|
||||
public string[]? Parents { get; }
|
||||
|
||||
[AbstractDataField]
|
||||
[NeverPushInheritance]
|
||||
public bool Abstract { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if cloning can be prevented by traits etc.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ForceCloning = true;
|
||||
|
||||
/// <summary>
|
||||
/// Which inventory slots will receive a copy of the original's clothing.
|
||||
/// Disabled when null.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for the equipment allowed to be copied.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Blacklist for the equipment allowed to be copied.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
/// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709
|
||||
/// <summary>
|
||||
/// Components to copy from the original to the clone.
|
||||
/// This only makes a shallow copy of datafields!
|
||||
/// If you need a deep copy or additional component initialization, then subscribe to CloningEvent instead!
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AlwaysPushInheritance]
|
||||
public HashSet<string> Components = new();
|
||||
}
|
||||
|
|
@ -65,6 +65,13 @@ namespace Content.Shared.Damage
|
|||
[DataField("radiationDamageTypes")]
|
||||
public List<ProtoId<DamageTypePrototype>> RadiationDamageTypeIDs = new() { "Radiation" };
|
||||
|
||||
/// <summary>
|
||||
/// Group types that affect the pain overlay.
|
||||
/// </summary>
|
||||
/// TODO: Add support for adding damage types specifically rather than whole damage groups
|
||||
[DataField]
|
||||
public List<ProtoId<DamageGroupPrototype>> PainDamageGroups = new() { "Brute", "Burn" };
|
||||
|
||||
[DataField]
|
||||
public Dictionary<MobState, ProtoId<HealthIconPrototype>> HealthIcons = new()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.FingerprintReader;
|
||||
|
||||
/// <summary>
|
||||
/// Component for checking if a user's fingerprint matches allowed fingerprints
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
[Access(typeof(FingerprintReaderSystem))]
|
||||
public sealed partial class FingerprintReaderComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The fingerprints that are allowed to access this entity.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public HashSet<string> AllowedFingerprints = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to ignore gloves when checking fingerprints.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool IgnoreGloves;
|
||||
|
||||
/// <summary>
|
||||
/// The popup to show when access is denied due to fingerprint mismatch.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? FailPopup;
|
||||
|
||||
/// <summary>
|
||||
/// The popup to show when access is denied due to wearing gloves.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? FailGlovesPopup;
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Forensics.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Shared.FingerprintReader;
|
||||
|
||||
// TODO: This has a lot of overlap with the AccessReaderSystem, maybe merge them in the future?
|
||||
public sealed class FingerprintReaderSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given user has fingerprint access to the target entity.
|
||||
/// </summary>
|
||||
/// <param name="target">The target entity.</param>
|
||||
/// <param name="user">User trying to gain access.</param>
|
||||
/// <returns>True if access was granted, otherwise false.</returns>
|
||||
[PublicAPI]
|
||||
public bool IsAllowed(Entity<FingerprintReaderComponent?> target, EntityUid user)
|
||||
{
|
||||
if (!Resolve(target, ref target.Comp, false))
|
||||
return true;
|
||||
|
||||
if (target.Comp.AllowedFingerprints.Count == 0)
|
||||
return true;
|
||||
|
||||
// Check for gloves first
|
||||
if (!target.Comp.IgnoreGloves && TryGetBlockingGloves(user, out var gloves))
|
||||
{
|
||||
if (target.Comp.FailGlovesPopup != null)
|
||||
_popup.PopupEntity(Loc.GetString(target.Comp.FailGlovesPopup, ("blocker", gloves)), target, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check fingerprint match
|
||||
if (!TryComp<FingerprintComponent>(user, out var fingerprint) || fingerprint.Fingerprint == null ||
|
||||
!target.Comp.AllowedFingerprints.Contains(fingerprint.Fingerprint))
|
||||
{
|
||||
if (target.Comp.FailPopup != null)
|
||||
_popup.PopupEntity(Loc.GetString(target.Comp.FailPopup), target, user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blocking gloves of a user. Gloves count as blocking if they hide fingerprints.
|
||||
/// </summary>
|
||||
/// <param name="user">Entity wearing the gloves.</param>
|
||||
/// <param name="blocker">The returned gloves, if they exist.</param>
|
||||
/// <returns>True if blocking gloves were found, otherwise False.</returns>
|
||||
[PublicAPI]
|
||||
public bool TryGetBlockingGloves(EntityUid user, [NotNullWhen(true)] out EntityUid? blocker)
|
||||
{
|
||||
blocker = null;
|
||||
|
||||
if (_inventory.TryGetSlotEntity(user, "gloves", out var gloves) && HasComp<FingerprintMaskComponent>(gloves))
|
||||
{
|
||||
blocker = gloves;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the allowed fingerprints for a fingerprint reader
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void SetAllowedFingerprints(Entity<FingerprintReaderComponent> target, HashSet<string> fingerprints)
|
||||
{
|
||||
target.Comp.AllowedFingerprints = fingerprints;
|
||||
Dirty(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an allowed fingerprint to a fingerprint reader
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void AddAllowedFingerprint(Entity<FingerprintReaderComponent> target, string fingerprint)
|
||||
{
|
||||
target.Comp.AllowedFingerprints.Add(fingerprint);
|
||||
Dirty(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an allowed fingerprint from a fingerprint reader
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void RemoveAllowedFingerprint(Entity<FingerprintReaderComponent> target, string fingerprint)
|
||||
{
|
||||
target.Comp.AllowedFingerprints.Remove(fingerprint);
|
||||
Dirty(target);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Buckle;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Storage.Components;
|
||||
|
|
@ -23,7 +24,7 @@ public sealed class FoldableSystem : EntitySystem
|
|||
|
||||
SubscribeLocalEvent<FoldableComponent, ComponentInit>(OnFoldableInit);
|
||||
SubscribeLocalEvent<FoldableComponent, ContainerGettingInsertedAttemptEvent>(OnInsertEvent);
|
||||
SubscribeLocalEvent<FoldableComponent, StoreMobInItemContainerAttemptEvent>(OnStoreThisAttempt);
|
||||
SubscribeLocalEvent<FoldableComponent, InsertIntoEntityStorageAttemptEvent>(OnStoreThisAttempt);
|
||||
SubscribeLocalEvent<FoldableComponent, StorageOpenAttemptEvent>(OnFoldableOpenAttempt);
|
||||
|
||||
SubscribeLocalEvent<FoldableComponent, StrapAttemptEvent>(OnStrapAttempt);
|
||||
|
|
@ -45,10 +46,8 @@ public sealed class FoldableSystem : EntitySystem
|
|||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref StoreMobInItemContainerAttemptEvent args)
|
||||
public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref InsertIntoEntityStorageAttemptEvent args)
|
||||
{
|
||||
args.Handled = true;
|
||||
|
||||
if (comp.IsFolded)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ namespace Content.Shared.Forensics.Components;
|
|||
public sealed partial class DnaComponent : Component
|
||||
{
|
||||
[DataField("dna"), AutoNetworkedField]
|
||||
public string DNA = String.Empty;
|
||||
public string? DNA;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public record struct TransferDnaEvent()
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// An event to generate and act upon new DNA for an entity.
|
||||
/// Raised on an entity when its DNA has been changed.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct GenerateDnaEvent()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences; // DeltaV
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.Preferences; // DeltaV, used for Metempsychosis, Fugitive, and Paradox Anomaly
|
||||
|
||||
namespace Content.Shared.Humanoid;
|
||||
|
||||
|
|
@ -100,10 +98,13 @@ public sealed partial class HumanoidAppearanceComponent : Component
|
|||
public HashSet<HumanoidVisualLayers> HideLayersOnEquip = [HumanoidVisualLayers.Hair];
|
||||
|
||||
/// <summary>
|
||||
/// DeltaV - let paradox anomaly be cloned
|
||||
/// Which markings the humanoid defaults to when nudity is toggled off.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HumanoidCharacterProfile? LastProfileLoaded;
|
||||
[DataField]
|
||||
public ProtoId<MarkingPrototype>? UndergarmentTop = new ProtoId<MarkingPrototype>("UndergarmentTopTanktop");
|
||||
|
||||
[DataField]
|
||||
public ProtoId<MarkingPrototype>? UndergarmentBottom = new ProtoId<MarkingPrototype>("UndergarmentBottomBoxers");
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ namespace Content.Shared.Humanoid
|
|||
Tail,
|
||||
Hair,
|
||||
FacialHair,
|
||||
UndergarmentTop,
|
||||
UndergarmentBottom,
|
||||
Chest,
|
||||
Underwear, // DeltaV
|
||||
Undershirt, // DeltaV
|
||||
Head,
|
||||
Snout,
|
||||
HeadSide, // side parts (i.e., frills)
|
||||
|
|
@ -21,7 +21,6 @@ namespace Content.Shared.Humanoid
|
|||
RArm,
|
||||
LArm,
|
||||
RHand,
|
||||
|
||||
LHand,
|
||||
RLeg,
|
||||
LLeg,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ namespace Content.Shared.Humanoid.Markings
|
|||
HeadSide,
|
||||
Snout,
|
||||
Chest,
|
||||
Underwear, // DeltaV
|
||||
Undershirt, // DeltaV
|
||||
UndergarmentTop,
|
||||
UndergarmentBottom,
|
||||
Arms,
|
||||
Legs,
|
||||
Tail,
|
||||
|
|
@ -34,9 +34,9 @@ namespace Content.Shared.Humanoid.Markings
|
|||
HumanoidVisualLayers.HeadTop => MarkingCategories.HeadTop,
|
||||
HumanoidVisualLayers.HeadSide => MarkingCategories.HeadSide,
|
||||
HumanoidVisualLayers.Snout => MarkingCategories.Snout,
|
||||
HumanoidVisualLayers.Undershirt => MarkingCategories.Undershirt, // DeltaV
|
||||
HumanoidVisualLayers.Underwear => MarkingCategories.Underwear, // DeltaV
|
||||
HumanoidVisualLayers.Chest => MarkingCategories.Chest,
|
||||
HumanoidVisualLayers.UndergarmentTop => MarkingCategories.UndergarmentTop,
|
||||
HumanoidVisualLayers.UndergarmentBottom => MarkingCategories.UndergarmentBottom,
|
||||
HumanoidVisualLayers.RArm => MarkingCategories.Arms,
|
||||
HumanoidVisualLayers.LArm => MarkingCategories.Arms,
|
||||
HumanoidVisualLayers.RHand => MarkingCategories.Arms,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue