diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index ae615fe2dee..3f89acaa866 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -62,6 +62,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnGetVisuals); + SubscribeLocalEvent(OnInventoryTemplateUpdated); SubscribeLocalEvent(OnVisualsChanged); SubscribeLocalEvent(OnDidUnequip); @@ -74,11 +75,7 @@ private void OnAppearanceUpdate(EntityUid uid, InventoryComponent component, ref if (args.Sprite == null) return; - var enumerator = _inventorySystem.GetSlotEnumerator((uid, component)); - while (enumerator.NextItem(out var item, out var slot)) - { - RenderEquipment(uid, item, slot.Name, component); - } + UpdateAllSlots(uid, component); // No clothing equipped -> make sure the layer is hidden, though this should already be handled by on-unequip. if (_sprite.LayerMapTryGet((uid, args.Sprite), HumanoidVisualLayers.StencilMask, out var layer, false)) @@ -88,6 +85,23 @@ private void OnAppearanceUpdate(EntityUid uid, InventoryComponent component, ref } } + private void OnInventoryTemplateUpdated(Entity ent, ref InventoryTemplateUpdated args) + { + UpdateAllSlots(ent.Owner, clothing: ent.Comp); + } + + private void UpdateAllSlots( + EntityUid uid, + InventoryComponent? inventoryComponent = null, + ClothingComponent? clothing = null) + { + var enumerator = _inventorySystem.GetSlotEnumerator((uid, inventoryComponent)); + while (enumerator.NextItem(out var item, out var slot)) + { + RenderEquipment(uid, item, slot.Name, inventoryComponent, clothingComponent: clothing); + } + } + private void OnGetVisuals(EntityUid uid, ClothingComponent item, GetEquipmentVisualsEvent args) { if (!TryComp(args.Equipee, out InventoryComponent? inventory)) diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index fd600b46670..d78926933ac 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -236,9 +236,23 @@ public void UIInventoryAltActivateItem(string slot, EntityUid uid) EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true)); } + protected override void UpdateInventoryTemplate(Entity ent) + { + base.UpdateInventoryTemplate(ent); + + if (TryComp(ent, out InventorySlotsComponent? inventorySlots)) + { + foreach (var slot in ent.Comp.Slots) + { + if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData)) + slotData.SlotDef = slot; + } + } + } + public sealed class SlotData { - public readonly SlotDefinition SlotDef; + public SlotDefinition SlotDef; public EntityUid? HeldEntity => Container?.ContainedEntity; public bool Blocked; public bool Highlighted; diff --git a/Content.Client/Overlays/EquipmentHudSystem.cs b/Content.Client/Overlays/EquipmentHudSystem.cs index 15b4ce1e026..b934622db39 100644 --- a/Content.Client/Overlays/EquipmentHudSystem.cs +++ b/Content.Client/Overlays/EquipmentHudSystem.cs @@ -14,6 +14,7 @@ public abstract class EquipmentHudSystem : EntitySystem where T : IComponent { [Dependency] private readonly IPlayerManager _player = default!; + [ViewVariables] protected bool IsActive; protected virtual SlotFlags TargetSlots => ~SlotFlags.POCKET; diff --git a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs index 65823a29822..007a0cc5cbe 100644 --- a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs +++ b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs @@ -133,7 +133,8 @@ private void UpdateModulePanel() _modules.Clear(); foreach (var module in chassis.ModuleContainer.ContainedEntities) { - var control = new BorgModuleControl(module, _entity); + var moduleComponent = _entity.GetComponent(module); + var control = new BorgModuleControl(module, _entity, !moduleComponent.DefaultModule); control.RemoveButtonPressed += () => { RemoveModuleButtonPressed?.Invoke(module); diff --git a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs index d5cf05ba63e..245425524ca 100644 --- a/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs +++ b/Content.Client/Silicons/Borgs/BorgModuleControl.xaml.cs @@ -9,7 +9,7 @@ public sealed partial class BorgModuleControl : PanelContainer { public Action? RemoveButtonPressed; - public BorgModuleControl(EntityUid entity, IEntityManager entityManager) + public BorgModuleControl(EntityUid entity, IEntityManager entityManager, bool canRemove) { RobustXamlLoader.Load(this); @@ -20,6 +20,7 @@ public BorgModuleControl(EntityUid entity, IEntityManager entityManager) { RemoveButtonPressed?.Invoke(); }; + RemoveButton.Visible = canRemove; } } diff --git a/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml new file mode 100644 index 00000000000..b3b519735d4 --- /dev/null +++ b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs new file mode 100644 index 00000000000..5a395749ada --- /dev/null +++ b/Content.Client/Silicons/Borgs/BorgSelectTypeMenu.xaml.cs @@ -0,0 +1,98 @@ +using System.Linq; +using Content.Client._CD.Silicons.Borgs.UI; +using Content.Client.UserInterface.Controls; +using Content.Client.UserInterface.Systems.Guidebook; +using Content.Shared._CD.Silicons; +using Content.Shared._CD.Silicons.Borgs; +using Content.Shared.Guidebook; +using Content.Shared.Silicons.Borgs; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; + +namespace Content.Client.Silicons.Borgs; + +/// +/// Menu used by borgs to select their type. +/// +/// +/// +[GenerateTypedNameReferences] +public sealed partial class BorgSelectTypeMenu : FancyWindow +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + private BorgTypePrototype? _selectedBorgType; + + public event Action>? ConfirmedBorgType; + public event Action? ConfirmedBorgSubtype; // CD event - borg subtypes + + [ValidatePrototypeId] + private static readonly List> GuidebookEntries = new() { "Cyborgs", "Robotics" }; + + public BorgSelectTypeMenu() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + var group = new ButtonGroup(); + foreach (var borgType in _prototypeManager.EnumeratePrototypes().OrderBy(PrototypeName)) + { + var button = new Button + { + Text = PrototypeName(borgType), + Group = group, + }; + button.OnPressed += _ => + { + _selectedBorgType = borgType; + UpdateInformation(borgType); + }; + SelectionsContainer.AddChild(button); + } + + ConfirmTypeButton.OnPressed += ConfirmButtonPressed; + HelpGuidebookIds = GuidebookEntries; + + // CD - borg subtype + ChassisSpriteSelection.SubtypeSelected += () => + { + ConfirmTypeButton.Disabled = false; + }; + } + + private void UpdateInformation(BorgTypePrototype prototype) + { + _selectedBorgType = prototype; + + InfoContents.Visible = true; + InfoPlaceholder.Visible = false; + + NameLabel.Text = PrototypeName(prototype); + DescriptionLabel.Text = Loc.GetString($"borg-type-{prototype.ID}-desc"); + ChassisView.SetPrototype(prototype.DummyPrototype); + + // CD changes below + if (_selectedBorgType != null) + { + ConfirmTypeButton.Disabled = true; + ChassisSpriteSelection.Update(_selectedBorgType); + } + } + + private void ConfirmButtonPressed(BaseButton.ButtonEventArgs obj) + { + if (_selectedBorgType == null) + return; + + ConfirmedBorgSubtype?.Invoke(ChassisSpriteSelection.SubtypePrototype); // CD - borg subtype + ConfirmedBorgType?.Invoke(_selectedBorgType); + } + + private static string PrototypeName(BorgTypePrototype prototype) + { + return Loc.GetString($"borg-type-{prototype.ID}-name"); + } +} diff --git a/Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs b/Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs new file mode 100644 index 00000000000..9453f5f3456 --- /dev/null +++ b/Content.Client/Silicons/Borgs/BorgSelectTypeUserInterface.cs @@ -0,0 +1,33 @@ +using Content.Shared._CD.Silicons; +using Content.Shared._CD.Silicons.Borgs; +using Content.Shared.Silicons.Borgs.Components; +using JetBrains.Annotations; +using Robust.Client.UserInterface; + +namespace Content.Client.Silicons.Borgs; + +/// +/// User interface used by borgs to select their type. +/// +/// +/// +/// +[UsedImplicitly] +public sealed class BorgSelectTypeUserInterface : BoundUserInterface +{ + [ViewVariables] + private BorgSelectTypeMenu? _menu; + + public BorgSelectTypeUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _menu = this.CreateWindow(); + _menu.ConfirmedBorgType += prototype => SendPredictedMessage(new BorgSelectTypeMessage(prototype)); + _menu.ConfirmedBorgSubtype += subtypePrototype => SendPredictedMessage(new BorgSelectSubtypeMessage(subtypePrototype?.ID)); // CD - borg subtypes + } +} diff --git a/Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs b/Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs new file mode 100644 index 00000000000..f3a10168424 --- /dev/null +++ b/Content.Client/Silicons/Borgs/BorgSwitchableTypeSystem.cs @@ -0,0 +1,97 @@ +using Content.Shared._CD.Silicons.Borgs; +using Content.Shared.Movement.Components; +using Content.Shared.Silicons.Borgs; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; +using Robust.Shared.Serialization.TypeSerializers.Implementations; + +namespace Content.Client.Silicons.Borgs; + +/// +/// Client side logic for borg type switching. Sets up primarily client-side visual information. +/// +/// +/// +public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem +{ + [Dependency] private readonly BorgSystem _borgSystem = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(AfterStateHandler); + SubscribeLocalEvent(OnComponentStartup); + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + UpdateEntityAppearance(ent); + } + + private void AfterStateHandler(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateEntityAppearance(ent); + } + + protected override void UpdateEntityAppearance( + Entity entity, + BorgTypePrototype prototype) + { + // CD - added checks to stop sprite state errors + if (!TryComp(entity, out var subtype) || + subtype.BorgSubtype != null) + return; + + if (TryComp(entity, out SpriteComponent? sprite)) + { + if (_resourceCache.TryGetResource( + SpriteSpecifierSerializer.TextureRoot / prototype.SpritePath, + out var res)) + { + sprite.BaseRSI = res.RSI; + } + _sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Body, prototype.SpriteBodyState); + _sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteToggleLightState); + } + + if (TryComp(entity, out BorgChassisComponent? chassis)) + { + _borgSystem.SetMindStates( + (entity.Owner, chassis), + prototype.SpriteHasMindState, + prototype.SpriteNoMindState); + + if (TryComp(entity, out AppearanceComponent? appearance)) + { + // Queue update so state changes apply. + _appearance.QueueUpdate(entity, appearance); + } + } + + if (prototype.SpriteBodyMovementState is { } movementState) + { + var spriteMovement = EnsureComp(entity); + spriteMovement.NoMovementLayers.Clear(); + spriteMovement.NoMovementLayers["movement"] = new PrototypeLayerData + { + State = prototype.SpriteBodyState, + }; + spriteMovement.MovementLayers.Clear(); + spriteMovement.MovementLayers["movement"] = new PrototypeLayerData + { + State = movementState, + }; + } + else + { + RemComp(entity); + } + + base.UpdateEntityAppearance(entity, prototype); + } +} diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs index 029cd0647d1..81689dbd606 100644 --- a/Content.Client/Silicons/Borgs/BorgSystem.cs +++ b/Content.Client/Silicons/Borgs/BorgSystem.cs @@ -93,4 +93,18 @@ private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref A _sprite.LayerSetRsiState((uid, sprite), MMIVisualLayers.Base, state); } } + + /// + /// Sets the sprite states used for the borg "is there a mind or not" indication. + /// + /// The entity and component to modify. + /// The state to use if the borg has a mind. + /// The state to use if the borg has no mind. + /// + /// + public void SetMindStates(Entity borg, string hasMindState, string noMindState) + { + borg.Comp.HasMindState = hasMindState; + borg.Comp.NoMindState = noMindState; + } } diff --git a/Content.Client/_CD/Silicons/Borgs/BorgSwitchableSubtypeSystem.cs b/Content.Client/_CD/Silicons/Borgs/BorgSwitchableSubtypeSystem.cs new file mode 100644 index 00000000000..06af0fb60cf --- /dev/null +++ b/Content.Client/_CD/Silicons/Borgs/BorgSwitchableSubtypeSystem.cs @@ -0,0 +1,106 @@ +using System.Linq; +using Content.Client.Silicons.Borgs; +using Content.Shared._CD.Silicons; +using Content.Shared._CD.Silicons.Borgs; +using Content.Shared.Movement.Components; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Utility; + +namespace Content.Client._CD.Silicons.Borgs; + +/// +/// Primarily handles the appearance aspects of the borg subtype. +/// +public sealed class BorgSwitchableSubtypeSystem : SharedBorgSwitchableSubtypeSystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly BorgSystem _borg = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly FixtureSystem _fixture = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentStartup); + SubscribeLocalEvent(OnAutoHandleEvent); + } + + private void OnAutoHandleEvent(Entity ent, ref AfterAutoHandleStateEvent args) + { + SelectBorgSubtype(ent); + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + SelectBorgSubtype(ent); + } + + protected override void UpdateEntityAppearance(Entity entity, BorgSubtypePrototype borgSubtypePrototype) + { + // LOT of copy pasted code from BorgSwitchableTypeSystem, but is probably necessary unless the upstream code + // is refactored + + // get our required components + var (owner, _) = entity; + if (!TryComp(entity, out var chassisSprite)) + return; + + // remove all existing layers + for (int i = chassisSprite.AllLayers.Count() - 1; i >= 0; i--) + { + _sprite.RemoveLayer((entity, chassisSprite), i); + } + + for (int i = 0; i < borgSubtypePrototype.LayerData.Length; i++) + { + var layerData = borgSubtypePrototype.LayerData[i]; + + layerData.RsiPath = borgSubtypePrototype.SpritePath?.ToString(); + if (borgSubtypePrototype.Offset != null) + layerData.Offset = borgSubtypePrototype.Offset; + _sprite.AddLayer((owner, chassisSprite), layerData, i); + } + + if (TryComp(entity, out var chassis)) + { + _borg.SetMindStates( + (entity.Owner, chassis), + borgSubtypePrototype.SpriteHasMindState, + borgSubtypePrototype.SpriteNoMindState); + + if (TryComp(entity, out AppearanceComponent? appearance)) + { + // Queue update so state changes apply. + _appearance.QueueUpdate(entity, appearance); + } + } + + if (borgSubtypePrototype.SpriteBodyMovementState is { } movementState) + { + var spriteMovement = EnsureComp(entity); + spriteMovement.NoMovementLayers.Clear(); + spriteMovement.NoMovementLayers["movement"] = new PrototypeLayerData + { + State = borgSubtypePrototype.SpriteBodyState, + }; + spriteMovement.MovementLayers.Clear(); + spriteMovement.MovementLayers["movement"] = new PrototypeLayerData + { + State = movementState, + }; + } + else + { + RemComp(entity); + } + + base.UpdateEntityAppearance(entity, borgSubtypePrototype); + } +} diff --git a/Content.Client/_CD/Silicons/Borgs/UI/ChassisSpriteSelection.xaml b/Content.Client/_CD/Silicons/Borgs/UI/ChassisSpriteSelection.xaml new file mode 100644 index 00000000000..71259ae2e09 --- /dev/null +++ b/Content.Client/_CD/Silicons/Borgs/UI/ChassisSpriteSelection.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/Content.Client/_CD/Silicons/Borgs/UI/ChassisSpriteSelection.xaml.cs b/Content.Client/_CD/Silicons/Borgs/UI/ChassisSpriteSelection.xaml.cs new file mode 100644 index 00000000000..5fae49c589b --- /dev/null +++ b/Content.Client/_CD/Silicons/Borgs/UI/ChassisSpriteSelection.xaml.cs @@ -0,0 +1,115 @@ +using System.Linq; +using System.Numerics; +using Content.Shared._CD.Silicons; +using Content.Shared._CD.Silicons.Borgs; +using Content.Shared.Silicons.Borgs; +using Robust.Client.AutoGenerated; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Utility; + +namespace Content.Client._CD.Silicons.Borgs.UI; + +[GenerateTypedNameReferences] +public sealed partial class ChassisSpriteSelection : Control +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IResourceCache _resource = default!; + + public BorgSubtypePrototype? SubtypePrototype; + public event Action? SubtypeSelected; + + public const int PrototypeViewSize = 2; + + public ChassisSpriteSelection() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } + + public void Update(BorgTypePrototype borgTypePrototype) + { + MainContainer.Visible = true; + + OptionsContainer.RemoveAllChildren(); + // Hardlight start - code simplification + var buttonGroup = new ButtonGroup(); + List