Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
@@ -1,5 +1,7 @@
using System.Numerics;
using Content.Client._Forge.Item;
using Content.Client.Items.Systems;
using Content.Shared._Forge.Item;
using Content.Shared.Item;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
Expand Down Expand Up @@ -165,24 +167,44 @@ protected override void Draw(DrawingHandleScreen handle)
// typically you'd divide by two, but since the textures are half a tile, this is done implicitly
var iconPosition = new Vector2((boundingGrid.Width + 1) * size.X + itemComponent.StoredOffset.X * 2,
(boundingGrid.Height + 1) * size.Y + itemComponent.StoredOffset.Y * 2);

var iconRotation = Location.Rotation + Angle.FromDegrees(itemComponent.StoredRotation);

if (itemComponent.StoredSprite is { } storageSprite)
{
var scale = 2 * UIScale;
var offset = (((Box2) boundingGrid).Size - Vector2.One) * size;
var sprite = _entityManager.System<SpriteSystem>().Frame0(storageSprite);

var spriteBox = new Box2Rotated(new Box2(0f, sprite.Height * scale, sprite.Width * scale, 0f), -iconRotation, Vector2.Zero);
var root = spriteBox.CalcBoundingBox().BottomLeft;
var pos = PixelPosition * 2
+ (Parent?.GlobalPixelPosition ?? Vector2.Zero)
+ offset;

handle.SetTransform(pos, iconRotation);
var box = new UIBox2(root, root + sprite.Size * scale);
handle.DrawTextureRect(sprite, box);
handle.SetTransform(GlobalPixelPosition, Angle.Zero);
// Forge-Change-Start: optional scaled storage draw for ForgeScaledStorageItemComponent only.
if (_entityManager.TryGetComponent(Entity, out ForgeScaledStorageItemComponent? forgeStorage)
&& ForgeScaledStorageDraw.TryDraw(
_entityManager,
itemComponent,
forgeStorage,
handle,
boundingGrid,
size,
PixelPosition,
Parent?.GlobalPixelPosition,
GlobalPixelPosition,
UIScale))
{
}
else
// Forge-Change-End
{
var scale = 2 * UIScale;
var offset = (((Box2) boundingGrid).Size - Vector2.One) * size;
var sprite = _entityManager.System<SpriteSystem>().Frame0(storageSprite);

var spriteBox = new Box2Rotated(new Box2(0f, sprite.Height * scale, sprite.Width * scale, 0f), -iconRotation, Vector2.Zero);
var root = spriteBox.CalcBoundingBox().BottomLeft;
var pos = PixelPosition * 2
+ (Parent?.GlobalPixelPosition ?? Vector2.Zero)
+ offset;

handle.SetTransform(pos, iconRotation);
var box = new UIBox2(root, root + sprite.Size * scale);
handle.DrawTextureRect(sprite, box);
handle.SetTransform(GlobalPixelPosition, Angle.Zero);
}
}
else
{
Expand Down
106 changes: 106 additions & 0 deletions Content.Client/_Forge/Clothing/HandsOpenMantleClothingSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Content.Client.Items.Systems;
using Content.Shared._Forge.Clothing;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;

namespace Content.Client._Forge.Clothing;

/// <summary>
/// Swaps equipped mantle RSI states when the wearer's hand contents change.
/// </summary>
public sealed class HandsOpenMantleClothingSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ItemSystem _item = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<HandsOpenMantleClothingComponent, GetEquipmentVisualsEvent>(OnGetVisuals,
after: [typeof(ClothingSystem)]);
SubscribeLocalEvent<HandsOpenMantleClothingComponent, GotEquippedEvent>(OnMantleEquipped);
SubscribeLocalEvent<HandsComponent, DidEquipHandEvent>(OnHandsChanged);
SubscribeLocalEvent<HandsComponent, DidUnequipHandEvent>(OnHandsChanged);
}

private void OnGetVisuals(Entity<HandsOpenMantleClothingComponent> ent, ref GetEquipmentVisualsEvent args)
{
if (!TryComp(ent, out ClothingComponent? clothing) || clothing.MappedLayer == null)
return;

if (!TryComp(args.Equipee, out HandsComponent? hands))
return;

var state = GetEquippedState(hands, ent.Comp);

foreach (var layer in args.Layers)
{
if (layer.Item1 != clothing.MappedLayer)
continue;

layer.Item2.State = state;
}
}

private void OnMantleEquipped(Entity<HandsOpenMantleClothingComponent> ent, ref GotEquippedEvent args)
{
_item.VisualsChanged(ent);
}

private void OnHandsChanged<T>(EntityUid uid, HandsComponent hands, T args)
where T : notnull
{
RefreshWearerMantle(uid);
}

private void RefreshWearerMantle(EntityUid wearer)
{
if (!_inventory.TryGetSlotEntity(wearer, "neck", out var mantle) || mantle == null)
return;

if (!HasComp<HandsOpenMantleClothingComponent>(mantle))
return;

_item.VisualsChanged(mantle.Value);
}

private static string GetEquippedState(HandsComponent hands, HandsOpenMantleClothingComponent comp)
{
var leftOccupied = false;
var rightOccupied = false;

foreach (var hand in hands.Hands.Values)
{
if (hand.HeldEntity == null)
continue;

switch (hand.Location)
{
case HandLocation.Left:
leftOccupied = true;
break;
case HandLocation.Right:
case HandLocation.Middle:
rightOccupied = true;
break;
}
}

if (leftOccupied && rightOccupied)
return comp.BothHandsState;

if (rightOccupied)
return comp.RightHandState;

if (leftOccupied)
return comp.LeftHandState;

return comp.ClosedState;
}
}
56 changes: 56 additions & 0 deletions Content.Client/_Forge/Item/ForgeScaledStorageDraw.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Numerics;
using Content.Shared._Forge.Item;
using Content.Shared.Item;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Maths;

namespace Content.Client._Forge.Item;

/// <summary>
/// Client-only storage UI drawing for entities with <see cref="ForgeScaledStorageItemComponent"/>.
/// </summary>
public static class ForgeScaledStorageDraw
{
public static bool TryDraw(
IEntityManager entityManager,
ItemComponent item,
ForgeScaledStorageItemComponent forgeStorage,
DrawingHandleScreen handle,
Box2i boundingGrid,
Vector2 size,
Vector2 pixelPosition,
Vector2? parentGlobalPixelPosition,
Vector2 globalPixelPosition,
float uiScale)
{
if (item.StoredSprite is not { } storageSprite)
return false;

var slotWide = boundingGrid.Width >= boundingGrid.Height;
var iconRotation = (slotWide ? Angle.Zero : Angle.FromDegrees(90))
+ Angle.FromDegrees(item.StoredRotation);
var baseScale = 2 * uiScale;
var drawScale = new Vector2(baseScale * forgeStorage.Scale.X, baseScale * forgeStorage.Scale.Y);
var offset = (((Box2) boundingGrid).Size - Vector2.One) * size;
var sprite = entityManager.System<SpriteSystem>().Frame0(storageSprite);
var spriteSize = sprite.Size * drawScale;

var gridPixelSize = new Vector2((boundingGrid.Width + 1) * size.X, (boundingGrid.Height + 1) * size.Y);
var gridCenter = pixelPosition * 2
+ (parentGlobalPixelPosition ?? Vector2.Zero)
+ offset
+ gridPixelSize / 2f;

var half = spriteSize / 2f;
var storedOffset = slotWide && forgeStorage.OffsetWide != Vector2i.Zero
? forgeStorage.OffsetWide
: item.StoredOffset;
var localOffset = new Vector2(storedOffset.X * 2f, storedOffset.Y * 2f);
handle.SetTransform(gridCenter, iconRotation);
handle.DrawTextureRect(sprite, new UIBox2(-half.X + localOffset.X, -half.Y + localOffset.Y, half.X + localOffset.X, half.Y + localOffset.Y));
handle.SetTransform(globalPixelPosition, Angle.Zero);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using Content.Server.Access.Components;
using Content.Server.Access.Systems;
using Content.Server.GameTicking;
using Content.Shared._Forge.Access.Components;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;

namespace Content.Server._Forge.Access.Systems;

/// <summary>
/// Applies visual-only job icon overrides after preset ID cards are configured.
/// </summary>
public sealed class ForgeIdCardJobIconOverrideSystem : EntitySystem
{
[Dependency] private IdCardSystem _idCard = default!;
[Dependency] private InventorySystem _inventory = default!;
[Dependency] private IPrototypeManager _prototypes = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<ForgeIdCardJobIconOverrideComponent, MapInitEvent>(
OnMapInit,
after: [typeof(PresetIdCardComponent)]);

SubscribeLocalEvent<ForgeIdCardJobIconOverrideComponent, EntInsertedIntoContainerMessage>(
OnInsertedIntoContainer);

SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(
OnJobsAssigned,
after: [typeof(PresetIdCardSystem)]);

SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete);

SubscribeLocalEvent<InventoryComponent, StartingGearEquippedEvent>(OnStartingGearEquipped);
}

private void OnMapInit(EntityUid uid, ForgeIdCardJobIconOverrideComponent comp, MapInitEvent args)
{
ApplyOverride(uid, comp);
}

private void OnInsertedIntoContainer(
EntityUid uid,
ForgeIdCardJobIconOverrideComponent comp,
EntInsertedIntoContainerMessage args)
{
ApplyOverride(uid, comp);
}

private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
{
ReapplyAllOverrides();
}

private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
{
Timer.Spawn(0, () => TryApplyOverrideForPlayer(args.Mob));
}

private void OnStartingGearEquipped(
EntityUid uid,
InventoryComponent component,
ref StartingGearEquippedEvent args)
{
// SetPdaAndIdCardData runs synchronously after StartingGearEquippedEvent and resets the icon from the mind job.
Timer.Spawn(0, () => TryApplyOverrideForPlayer(uid));
}

private void ReapplyAllOverrides()
{
var query = EntityQueryEnumerator<ForgeIdCardJobIconOverrideComponent>();
while (query.MoveNext(out var uid, out var comp))
{
ApplyOverride(uid, comp);
}
}

private void TryApplyOverrideForPlayer(EntityUid player)
{
if (!Exists(player))
return;

if (!_inventory.TryGetSlotEntity(player, "id", out var idUid))
return;

var cardId = idUid.Value;
if (TryComp<PdaComponent>(idUid, out var pda) && pda.ContainedId != null)
cardId = pda.ContainedId.Value;

if (TryComp<ForgeIdCardJobIconOverrideComponent>(cardId, out var comp))
ApplyOverride(cardId, comp);
}

private void ApplyOverride(EntityUid uid, ForgeIdCardJobIconOverrideComponent comp)
{
if (!_prototypes.TryIndex(comp.JobIcon, out JobIconPrototype? icon))
return;

_idCard.TryChangeJobIcon(uid, icon);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;

namespace Content.Shared._Forge.Access.Components;

/// <summary>
/// Overrides the job icon on an ID card without changing its preset job (access, title, playtime role).
/// </summary>
[RegisterComponent]
public sealed partial class ForgeIdCardJobIconOverrideComponent : Component
{
[DataField(required: true)]
public ProtoId<JobIconPrototype> JobIcon;
}
23 changes: 23 additions & 0 deletions Content.Shared/_Forge/Clothing/HandsOpenMantleClothingComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Robust.Shared.GameStates;

namespace Content.Shared._Forge.Clothing;

/// <summary>
/// Neck mantle that switches equipped sprite based on what the wearer holds in their hands.
/// Visual updates are handled on the client (HandsOpenMantleClothingSystem).
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class HandsOpenMantleClothingComponent : Component
{
[DataField]
public string ClosedState = "equipped-NECK";

[DataField]
public string RightHandState = "open-1";

[DataField]
public string BothHandsState = "open-2";

[DataField]
public string LeftHandState = "open-3";
}
Loading
Loading