Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chameleon controller implant (Clothing fast switch) #33887

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Linq;
using Content.Shared.Clothing;
using Content.Shared.Implants;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;

namespace Content.Client.Implants.UI;

[UsedImplicitly]
public sealed class ChameleonControllerBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;

[ViewVariables]
private ChameleonControllerMenu? _menu;

public ChameleonControllerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}

protected override void Open()
{
base.Open();

_menu = this.CreateWindow<ChameleonControllerMenu>();
_menu.OnIdSelected += OnIdSelected;
}

protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not ChameleonControllerBuiState)
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
return;

var jobProtos = _prototypeManager.EnumeratePrototypes<JobPrototype>();
var validList = new List<JobPrototype>();

// Only add stuff that actually has clothing! We don't want stuff like AI or borgs.
foreach (var job in jobProtos)
{
if (job.StartingGear == null || !_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
continue;

validList.Add(job);
}

_menu?.UpdateState(validList.AsEnumerable());
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
}

private void OnIdSelected(ProtoId<JobPrototype> selectedJob)
{
SendMessage(new ChameleonControllerSelectedJobMessage(selectedJob));
}
}
12 changes: 12 additions & 0 deletions Content.Client/Implants/UI/ChameleonControllerMenu.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'chameleon-controller-ui-window-name'}"
MinSize="250 300"
SetSize="675 465">
<BoxContainer Orientation="Vertical" Margin="7 0 0 0">
<ScrollContainer VerticalExpand="True">
<GridContainer Name="Grid" Columns="3" Margin="0 5" >
</GridContainer>
</ScrollContainer>
</BoxContainer>
</controls:FancyWindow>
77 changes: 77 additions & 0 deletions Content.Client/Implants/UI/ChameleonControllerMenu.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;

namespace Content.Client.Implants.UI;

[GenerateTypedNameReferences]
public sealed partial class ChameleonControllerMenu : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;

private readonly SpriteSystem _sprite;
public event Action<ProtoId<JobPrototype>>? OnIdSelected;
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved

// List of all the job protos that you can select!
private IEnumerable<JobPrototype> _jobPrototypes = [];

public ChameleonControllerMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_sprite = _entityManager.System<SpriteSystem>();
}

public void UpdateState(IEnumerable<JobPrototype> jobs)
{
_jobPrototypes = jobs;
UpdateGrid();
}

private void UpdateGrid()
{
ClearGrid();

foreach (var job in _jobPrototypes)
{
if (!_prototypeManager.TryIndex(job.Icon, out var jobIconProto))
continue;

var boxContainer = new BoxContainer();

var button = new Button
{
HorizontalExpand = true,
StyleClasses = {StyleBase.ButtonSquare},
ToolTip = Loc.GetString(job.Name),
Text = Loc.GetString(job.Name),
Margin = new Thickness(0, 0, 15, 0),
};

var jobIconTexture = new TextureRect
{
Texture = _sprite.Frame0(jobIconProto.Icon),
TextureScale = new Vector2(2.5f, 2.5f),
Stretch = TextureRect.StretchMode.KeepCentered,
Margin = new Thickness(0, 0, 5, 0),
};
boxContainer.AddChild(jobIconTexture);
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
boxContainer.AddChild(button);

button.OnPressed += _ => OnIdSelected?.Invoke(job);
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
Grid.AddChild(boxContainer);
}
}

private void ClearGrid()
{
Grid.RemoveAllChildren();
}
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
}
90 changes: 90 additions & 0 deletions Content.Server/Implants/ChameleonControllerSystem.cs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed there's a lot of shared/duplicate code between this and the SetOutfitCommand

https://github.com/space-wizards/space-station-14/blob/782a2978f0b34f9c74b209a072a175e84e645b24/Content.Server/Administration/Commands/SetOutfitCommand.cs

It might be useful to look or even directly integrate the two as they do a very similar job.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this on discord, the issue right now is that although there is a decent amount of duplicated code / similar code, the it would actually be relatively difficult to combine the functions. The big issue is that the set outfit command doesn't go off of jobs and is based off StartingGearPrototype which kind of messes stuff up. I might be able to somehow combine them partially but I don't really think the complexity would decrease that much unfortunately 😔

If someone with more experience with loadouts could give some pointers or anyone has ideas, I'll totally combine them but for now I'm just going to leave it separate.

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Content.Server.Clothing.Systems;
using Content.Server.Preferences.Managers;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;

namespace Content.Server.Implants;

public sealed class ChameleonControllerSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedStationSpawningSystem _stationSpawningSystem = default!;
[Dependency] private readonly ChameleonClothingSystem _chameleonClothingSystem = default!;

public override void Initialize()
{
SubscribeLocalEvent<SubdermalImplantComponent, ChameleonControllerSelectedJobMessage>(OnSelected);
}

private void OnSelected(EntityUid uid, SubdermalImplantComponent component, ChameleonControllerSelectedJobMessage args)
{
if (component.ImplantedEntity == null || !HasComp<ChameleonControllerImplantComponent>(uid))
return;

ChangeChameleonClothingToJob(component.ImplantedEntity.Value, args.SelectedJob);
}
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Switches all the chamelean clothing that the implant user is wearing to look like the selected job.
/// </summary>
private void ChangeChameleonClothingToJob(EntityUid user, ProtoId<JobPrototype> job)
{
if (!_proto.TryIndex(job, out var jobPrototype))
return;

if (!_proto.TryIndex(jobPrototype.StartingGear, out var startingGearPrototype))
return;

if (!TryComp<ActorComponent>(user, out var actorComponent))
return;

var session = actorComponent.PlayerSession;
var userId = actorComponent.PlayerSession.UserId;
var preferencesManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = preferencesManager.GetPreferences(userId);

if (prefs.SelectedCharacter is not HumanoidCharacterProfile profile)
return;

if (!_inventorySystem.TryGetSlots(user, out var slots))
return;

// Does the job even exist?
var jobProtoId = LoadoutSystem.GetJobPrototype(job.Id);
if (!_proto.HasIndex<RoleLoadoutPrototype>(jobProtoId))
return;

profile.Loadouts.TryGetValue(jobProtoId, out var loadout);
loadout ??= new RoleLoadout(jobProtoId);
loadout.SetDefault(profile, session, _proto); // only sets the default if the player has no loadout

if (!_proto.HasIndex(loadout.Role))
return;

// Go through all the slots on the player
foreach (var slot in slots)
{
_inventorySystem.TryGetSlotEntity(user, slot.Name, out var containedUid);
// If there isn't anything there, or it isn't chameleon clothing.
if (containedUid == null || !TryComp<ChameleonClothingComponent>(containedUid, out var chameleonClothingComponent))
continue;

// Either get the gear from the loadout, or the starting gear.
var proto = _stationSpawningSystem.GetGearForSlot(loadout, slot.Name) ?? ((IEquipmentLoadout) startingGearPrototype).GetGear(slot.Name);
if (proto == string.Empty)
continue;

_chameleonClothingSystem.SetSelectedPrototype(containedUid.Value, proto, true, chameleonClothingComponent);
}
}
}
44 changes: 44 additions & 0 deletions Content.Shared/Implants/ChameleonControllerImplantComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Robust.Shared.GameStates;
using Content.Shared.Actions;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved

namespace Content.Shared.Implants;

/// <summary>
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
/// Will allow anyone implanted with the implant to have more control over their chameleon clothing and items.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ChameleonControllerImplantComponent : Component
{

beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// This is sent when someone clicks on the hud icon and will open the menu.
/// </summary>
public sealed partial class ChameleonControllerOpenMenuEvent : InstantActionEvent;

[Serializable, NetSerializable]
public enum ChameleonControllerKey : byte
{
Key,
}

[Serializable, NetSerializable]
public sealed class ChameleonControllerBuiState : BoundUserInterfaceState;

beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Triggered when the user clicks on a job in the menu.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChameleonControllerSelectedJobMessage : BoundUserInterfaceMessage
{
public readonly ProtoId<JobPrototype> SelectedJob;

public ChameleonControllerSelectedJobMessage(ProtoId<JobPrototype> selectedJob)
{
SelectedJob = selectedJob;
}
}
25 changes: 25 additions & 0 deletions Content.Shared/Implants/SharedChameleonControllerSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Content.Shared.Implants;

public sealed class SharedChameleonControllerSystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;

public override void Initialize()
{
SubscribeLocalEvent<ChameleonControllerOpenMenuEvent>(OpenUI);
}

private void OpenUI(ChameleonControllerOpenMenuEvent ev)
{
var implant = ev.Action.Comp.Container;

if (!HasComp<ChameleonControllerImplantComponent>(implant))
return;

if (!_uiSystem.HasUi(implant.Value, ChameleonControllerKey.Key))
return;

_uiSystem.OpenUi(implant.Value, ChameleonControllerKey.Key, ev.Performer);
_uiSystem.SetUiState(implant.Value, ChameleonControllerKey.Key, new ChameleonControllerBuiState());
}
}
27 changes: 27 additions & 0 deletions Content.Shared/Station/SharedStationSpawningSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,31 @@ public void EquipStartingGear(EntityUid entity, IEquipmentLoadout? startingGear,
RaiseLocalEvent(entity, ref ev);
}
}

/// <summary>
/// Gets all the gear for a given slot when passed a loadout.
/// </summary>
/// <param name="loadout">The loadout to look through.</param>
/// <param name="slot">The slot that you want the clothing for.</param>
/// <returns>
/// If there is a value for the given slot, it will return the proto id for that slot.
/// If nothing was found, will return null
/// </returns>
public string? GetGearForSlot(RoleLoadout loadout, string slot)
{
foreach (var group in loadout.SelectedLoadouts)
{
foreach (var items in group.Value)
{
if (!PrototypeManager.TryIndex(items.Prototype, out var loadoutPrototype))
return null;

var gear = ((IEquipmentLoadout) loadoutPrototype).GetGear(slot);
if (gear != string.Empty)
return gear;
}
}

return null;
}
}
1 change: 1 addition & 0 deletions Resources/Locale/en-US/implant/chameleon-controller.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
chameleon-controller-ui-window-name = Chameleon controls
10 changes: 10 additions & 0 deletions Resources/Prototypes/Actions/types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,13 @@
itemIconStyle: NoItem
useDelay: 1 # emote spam
event: !type:ToggleActionEvent

- type: entity
id: ActionChameleonController
name: Control clothing
description: Change your entire outfit fast!
components:
- type: InstantAction
icon: { sprite: Actions/Implants/implants.rsi, state: chameleon }
itemIconStyle: BigAction
event: !type:ChameleonControllerOpenMenuEvent
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I also noticed that many of the other implant actions have a set number of charges. Do you think unlimited would be fine balancing-wise?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the usage rate of chameleon clothing (Its really low) yeah I don't think is an issue at all! This was never intended to be a specific amount of uses

1 change: 1 addition & 0 deletions Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
- id: ClothingHeadsetChameleon
- id: ClothingShoesChameleon
- id: BarberScissors
- id: ChameleonControllerImplanter

- type: entity
parent: ClothingBackpackDuffelSyndicateBundle
Expand Down
8 changes: 8 additions & 0 deletions Resources/Prototypes/Entities/Objects/Misc/implanters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,11 @@
components:
- type: Implanter
implant: MindShieldImplant

- type: entity
id: ChameleonControllerImplanter
suffix: chameleon controller
parent: BaseImplantOnlyImplanterSyndi
components:
- type: Implanter
implant: ChameleonControllerImplant
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 15 additions & 0 deletions Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,18 @@
- type: Tag
tags:
- MindShield

- type: entity
categories: [ HideSpawnMenu, Spawner ]
parent: BaseSubdermalImplant
id: ChameleonControllerImplant
name: chameleon controller implant
description: This implant allows you to instantly change the look of all your chameleon clothing in an instant.
beck-thompson marked this conversation as resolved.
Show resolved Hide resolved
components:
- type: ChameleonControllerImplant
- type: SubdermalImplant
implantAction: ActionChameleonController
- type: UserInterface
interfaces:
enum.ChameleonControllerKey.Key:
type: ChameleonControllerBoundUserInterface
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading