From e092203d1134521f95c44c424bfa57f1ec7ffe53 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:53:25 +0300 Subject: [PATCH 01/21] Frictionfull Space (#514) # Description Makes it so that the station and the ATS get a very tiny bit of friction to prevent cargo tech pros from sending either of those out of this galaxy cluster (which has actually happened multiple times on two servers and required either admin intervention or early round ending). # Technical details Added a PassiveDampeningComponent which defines how much friction an entity receives while in 0g. FrictionRemoverSystem was updated to try to fetch this component from an entity before updating its dampening. A new system was added to automatically add this component (if it's not already defined) to all station grids. # Media See the #when-you-code-it channel for a preview. It's kinda hard to demonstrate, but after a few minutes, stations and the ATS come to an almost complete stop. # Changelog :cl: - tweak: Space stations now have a tiny bit of velocity dampening to prevent them from being flunged into the void. --- .../Station/Systems/StationDampeningSystem.cs | 28 +++++++++++++++++++ .../Physics/FrictionRemoverSystem.cs | 13 +++++++-- .../Physics/PassiveDampeningComponent.cs | 18 ++++++++++++ Resources/Maps/Shuttles/trading_outpost.yml | 3 ++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Content.Server/Station/Systems/StationDampeningSystem.cs create mode 100644 Content.Shared/Physics/PassiveDampeningComponent.cs diff --git a/Content.Server/Station/Systems/StationDampeningSystem.cs b/Content.Server/Station/Systems/StationDampeningSystem.cs new file mode 100644 index 00000000000..f499127031e --- /dev/null +++ b/Content.Server/Station/Systems/StationDampeningSystem.cs @@ -0,0 +1,28 @@ +using Content.Server.Station.Events; +using Content.Shared.Physics; + +namespace Content.Server.Station.Systems; + +public sealed class StationDampeningSystem : EntitySystem +{ + public override void Initialize() + { + SubscribeLocalEvent(OnInitStation); + } + + private void OnInitStation(ref StationPostInitEvent ev) + { + foreach (var grid in ev.Station.Comp.Grids) + { + // If the station grid doesn't have defined dampening, give it a small dampening by default + // This will ensure cargo tech pros won't fling the station 1000 megaparsec away from the galaxy + if (!TryComp(grid, out var dampening)) + { + dampening = AddComp(grid); + dampening.Enabled = true; + dampening.LinearDampening = 0.01f; + dampening.AngularDampening = 0.01f; + } + } + } +} diff --git a/Content.Shared/Physics/FrictionRemoverSystem.cs b/Content.Shared/Physics/FrictionRemoverSystem.cs index 65bbe9e4d23..c8d7521eb01 100644 --- a/Content.Shared/Physics/FrictionRemoverSystem.cs +++ b/Content.Shared/Physics/FrictionRemoverSystem.cs @@ -1,3 +1,4 @@ +using Robust.Shared.Map.Components; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; @@ -19,7 +20,15 @@ public override void Initialize() private void RemoveDampening(EntityUid uid, PhysicsComponent component, PhysicsSleepEvent args) { - _physics.SetAngularDamping(uid, component, 0f, false); - _physics.SetLinearDamping(uid, component, 0f); + var linear = 0f; + var angular = 0f; + if (TryComp(uid, out var dampening) && dampening.Enabled) + { + linear = dampening.LinearDampening; + angular = dampening.AngularDampening; + } + + _physics.SetAngularDamping(uid, component, angular, false); + _physics.SetLinearDamping(uid, component, linear); } } diff --git a/Content.Shared/Physics/PassiveDampeningComponent.cs b/Content.Shared/Physics/PassiveDampeningComponent.cs new file mode 100644 index 00000000000..834569195ee --- /dev/null +++ b/Content.Shared/Physics/PassiveDampeningComponent.cs @@ -0,0 +1,18 @@ +namespace Content.Shared.Physics; + +/// +/// A component that allows an entity to have friction (linear and angular dampening) +/// even when not being affected by gravity. +/// +[RegisterComponent] +public sealed partial class PassiveDampeningComponent : Component +{ + [DataField] + public bool Enabled = true; + + [DataField] + public float LinearDampening = 0.2f; + + [DataField] + public float AngularDampening = 0.2f; +} diff --git a/Resources/Maps/Shuttles/trading_outpost.yml b/Resources/Maps/Shuttles/trading_outpost.yml index f040d58253d..7b968b5c13d 100644 --- a/Resources/Maps/Shuttles/trading_outpost.yml +++ b/Resources/Maps/Shuttles/trading_outpost.yml @@ -60,6 +60,9 @@ entities: linearDamping: 0.05 fixedRotation: False bodyType: Dynamic + - type: PassiveDampening # To prevent cargotechs from flingling it away. + linearDampening: 0.01 + angularDampening: 0.01 - type: Fixtures fixtures: {} - type: OccluderTree From 81dd78259e66b6fda06fcb7c4b76b365eb4f652a Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 16:53:46 +0000 Subject: [PATCH 02/21] Automatic Changelog Update (#514) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c4056561c9c..847b9c90fbe 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4285,3 +4285,11 @@ Entries: message: Equipping clothing using the Z key works correctly again. id: 6136 time: '2024-07-03T19:48:29.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Tweak + message: >- + Space stations now have a tiny bit of velocity dampening to prevent them + from being flunged into the void. + id: 6137 + time: '2024-07-05T16:53:25.0000000+00:00' From 58be8504857c54d311e9100ee89b921d2437ee35 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:28:44 +0300 Subject: [PATCH 03/21] Port All Carrying/PseudoItem/EscapeInventory Tweaks From DeltaV (#484) # Description This cherry-picks the following two PRs of mine from delta-v: - https://github.com/DeltaV-Station/Delta-v/pull/1118 (this one is a port of two other frontier PRs, see the original description for details) - https://github.com/DeltaV-Station/Delta-v/pull/1232 Encompassing a total of 8 distinct changes: 1. Fixes dropping the carried person when walking to a different grid (from station to shuttle or vice versa. Walking into space however will still make you drop them) and also makes sure that pressing shift while being carried does not make you escape. 2. Ensures that the carried person is always centered relative to the parent (under certain conditions, such as walking near a gravitational anomaly, their position can change, and that leads to really weird effects) 3. Fixes the mass contest in CarryingSystem that caused stronger entities to take longer to escape than weaker ones. 4. Adds popups for when you're getting picked up or being stuffed into a bag as a pseudo-item (e.g. a felinid) 5. Adds an action to stop escaping an inventory. This action gets added to your action bar when you attempt escaping and gets removed when you stop or escape. It applies both to carrying and items (hampsters, felinids, whatever else). 6. Adds a sleep action for pseudo-items stuffed inside a suitable bag. The bag must have a special component, which is added to the base backpack item and thus inherited by all soft bags (duffels, satchels, etc). Contrary to a popular belief, sleeping IS PURELY COSMETICAL and does not provide healing. (Beds provide healing when you buckle into them and that healing does not depend on whether or not you're sleeping) 7. Makes it so that when you try to take a pseudo-item out of the bag (e.g. a felinid), you automatically try to carry them (if you don't have enough free hands, they will be dropped on the floor like usually), and enables you to insert the carried person into a bag, but only if they're a pseudo-item (e.g. felinid). 8. Allows pseudoitems to be inserted into bags even when there are other items (as long as there's enough space) --- ## For technical details and video showcases, see the original PRs This PR is split into separate commits so different parts can be reverted if deemed unneccessary. --- # Changelog :cl: - fix: Carrying is less likely to behave erratically or suddenly interrupt now. - add: You can now see when someone is trying to pick you up, and also you can interrupt your attempt at escaping from their hands or inventory. - add: You can now properly take Felinids out of bags and place them inside. - add: Scientists have discovered that Felinids can sleep in bags. --- .../Nyanotrasen/Carrying/CarryingSystem.cs | 116 +++++++++++- .../Item/PseudoItem/PseudoItemSystem.cs | 30 +++- .../Resist/CanEscapeInventoryComponent.cs | 6 + .../Resist/EscapeInventorySystem.cs | 26 +++ .../PseudoItem/AllowsSleepInsideComponent.cs | 9 + .../Item/PseudoItem/PseudoItemComponent.cs | 14 +- .../SharedPseudoItemSystem.Checks.cs | 166 ++---------------- .../Item/PseudoItem/SharedPseudoItemSystem.cs | 24 ++- .../Resist/EscapeInventoryCancelEvent.cs | 5 + .../Locale/en-US/actions/actions/sleep.ftl | 2 + .../en-US/nyanotrasen/carrying/carry.ftl | 1 + Resources/Prototypes/Actions/misc.yml | 10 ++ .../Entities/Clothing/Back/backpacks.yml | 3 +- .../escapeinventory.rsi/cancel-escape.png | Bin 0 -> 559 bytes .../Actions/escapeinventory.rsi/meta.json | 14 ++ 15 files changed, 259 insertions(+), 167 deletions(-) create mode 100644 Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs create mode 100644 Content.Shared/Resist/EscapeInventoryCancelEvent.cs create mode 100644 Resources/Prototypes/Actions/misc.yml create mode 100644 Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png create mode 100644 Resources/Textures/Actions/escapeinventory.rsi/meta.json diff --git a/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs b/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs index bb071334fa1..103731b1b04 100644 --- a/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs +++ b/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using System.Threading; using Content.Server.DoAfter; using Content.Server.Body.Systems; @@ -5,6 +6,7 @@ using Content.Server.Resist; using Content.Server.Popups; using Content.Server.Inventory; +using Content.Server.Nyanotrasen.Item.PseudoItem; using Content.Shared.Climbing; // Shared instead of Server using Content.Shared.Mobs; using Content.Shared.DoAfter; @@ -23,9 +25,12 @@ using Content.Shared.Standing; using Content.Shared.ActionBlocker; using Content.Shared.Inventory.VirtualItem; +using Content.Shared.Item; using Content.Shared.Throwing; using Content.Shared.Physics.Pull; using Content.Shared.Mobs.Systems; +using Content.Shared.Nyanotrasen.Item.PseudoItem; +using Content.Shared.Storage; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; @@ -44,11 +49,13 @@ public sealed class CarryingSystem : EntitySystem [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; [Dependency] private readonly RespiratorSystem _respirator = default!; + [Dependency] private readonly PseudoItemSystem _pseudoItem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent>(AddCarryVerb); + SubscribeLocalEvent>(AddInsertCarriedVerb); SubscribeLocalEvent(OnVirtualItemDeleted); SubscribeLocalEvent(OnThrow); SubscribeLocalEvent(OnParentChanged); @@ -64,7 +71,6 @@ public override void Initialize() SubscribeLocalEvent(OnDoAfter); } - private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsEvent args) { if (!args.CanInteract || !args.CanAccess) @@ -97,6 +103,33 @@ private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsE args.Verbs.Add(verb); } + private void AddInsertCarriedVerb(EntityUid uid, CarryingComponent component, GetVerbsEvent args) + { + // If the person is carrying someone, and the carried person is a pseudo-item, and the target entity is a storage, + // then add an action to insert the carried entity into the target + var toInsert = args.Using; + if (toInsert is not { Valid: true } || !args.CanAccess || !TryComp(toInsert, out var pseudoItem)) + return; + + if (!TryComp(args.Target, out var storageComp)) + return; + + if (!_pseudoItem.CheckItemFits((toInsert.Value, pseudoItem), (args.Target, storageComp))) + return; + + InnateVerb verb = new() + { + Act = () => + { + DropCarried(uid, toInsert.Value); + _pseudoItem.TryInsert(args.Target, toInsert.Value, pseudoItem, storageComp); + }, + Text = Loc.GetString("action-name-insert-other", ("target", toInsert)), + Priority = 2 + }; + args.Verbs.Add(verb); + } + /// /// Since the carried entity is stored as 2 virtual items, when deleted we want to drop them. /// @@ -125,7 +158,12 @@ private void OnThrow(EntityUid uid, CarryingComponent component, BeforeThrowEven private void OnParentChanged(EntityUid uid, CarryingComponent component, ref EntParentChangedMessage args) { - if (Transform(uid).MapID != args.OldMapId) + var xform = Transform(uid); + if (xform.MapID != args.OldMapId) + return; + + // Do not drop the carried entity if the new parent is a grid + if (xform.ParentUid == xform.GridUid) return; DropCarried(uid, component.Carried); @@ -158,9 +196,13 @@ private void OnMoveInput(EntityUid uid, BeingCarriedComponent component, ref Mov if (!TryComp(uid, out var escape)) return; + if (!args.HasDirectionalMovement) + return; + if (_actionBlockerSystem.CanInteract(uid, component.Carrier)) { - _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, MassContest(uid, component.Carrier)); + // Note: the mass contest is inverted because weaker entities are supposed to take longer to escape + _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, MassContest(component.Carrier, uid)); } } @@ -209,12 +251,7 @@ private void OnDoAfter(EntityUid uid, CarriableComponent component, CarryDoAfter } private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableComponent component) { - TimeSpan length = TimeSpan.FromSeconds(3); - - var mod = MassContest(carrier, carried); - - if (mod != 0) - length /= mod; + TimeSpan length = GetPickupDuration(carrier, carried); if (length >= TimeSpan.FromSeconds(9)) { @@ -236,6 +273,9 @@ private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableCo }; _doAfterSystem.TryStartDoAfter(args); + + // Show a popup to the person getting picked up + _popupSystem.PopupEntity(Loc.GetString("carry-started", ("carrier", carrier)), carried, carried); } private void Carry(EntityUid carrier, EntityUid carried) @@ -260,6 +300,26 @@ private void Carry(EntityUid carrier, EntityUid carried) _actionBlockerSystem.UpdateCanMove(carried); } + public bool TryCarry(EntityUid carrier, EntityUid toCarry, CarriableComponent? carriedComp = null) + { + if (!Resolve(toCarry, ref carriedComp, false)) + return false; + + if (!CanCarry(carrier, toCarry, carriedComp)) + return false; + + // The second one means that carrier is a pseudo-item and is inside a bag. + if (HasComp(carrier) || HasComp(carrier)) + return false; + + if (GetPickupDuration(carrier, toCarry) > TimeSpan.FromSeconds(9)) + return false; + + Carry(carrier, toCarry); + + return true; + } + public void DropCarried(EntityUid carrier, EntityUid carried) { RemComp(carrier); // get rid of this first so we don't recusrively fire that event @@ -323,5 +383,43 @@ private float MassContest(EntityUid roller, EntityUid target, PhysicsComponent? return rollerPhysics.FixturesMass / targetPhysics.FixturesMass; } + + private TimeSpan GetPickupDuration(EntityUid carrier, EntityUid carried) + { + var length = TimeSpan.FromSeconds(3); + + var mod = MassContest(carrier, carried); + if (mod != 0) + length /= mod; + + return length; + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var carried, out var comp)) + { + var carrier = comp.Carrier; + if (carrier is not { Valid: true } || carried is not { Valid: true }) + continue; + + // SOMETIMES - when an entity is inserted into disposals, or a cryosleep chamber - it can get re-parented without a proper reparent event + // when this happens, it needs to be dropped because it leads to weird behavior + if (Transform(carried).ParentUid != carrier) + { + DropCarried(carrier, carried); + continue; + } + + // Make sure the carried entity is always centered relative to the carrier, as gravity pulls can offset it otherwise + var xform = Transform(carried); + if (!xform.LocalPosition.Equals(Vector2.Zero)) + { + xform.LocalPosition = Vector2.Zero; + } + } + query.Dispose(); + } } } diff --git a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs index 76cfe7d904b..6df387e6ba8 100644 --- a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs +++ b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs @@ -1,6 +1,9 @@ -using Content.Server.DoAfter; +using Content.Server.Carrying; +using Content.Server.DoAfter; using Content.Server.Item; +using Content.Server.Popups; using Content.Server.Storage.EntitySystems; +using Content.Shared.Bed.Sleep; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Item; @@ -17,12 +20,14 @@ public sealed class PseudoItemSystem : SharedPseudoItemSystem [Dependency] private readonly StorageSystem _storage = default!; [Dependency] private readonly ItemSystem _item = default!; [Dependency] private readonly DoAfterSystem _doAfter = default!; - + [Dependency] private readonly CarryingSystem _carrying = default!; + [Dependency] private readonly PopupSystem _popup = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent>(AddInsertAltVerb); + SubscribeLocalEvent(OnTrySleeping); } private void AddInsertAltVerb(EntityUid uid, PseudoItemComponent component, GetVerbsEvent args) @@ -53,4 +58,25 @@ private void AddInsertAltVerb(EntityUid uid, PseudoItemComponent component, GetV }; args.Verbs.Add(verb); } + + protected override void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args) + { + // Try to pick the entity up instead first + if (args.User != args.Item && _carrying.TryCarry(args.User, uid)) + { + args.Cancel(); + return; + } + + // If could not pick up, just take it out onto the ground as per default + base.OnGettingPickedUpAttempt(uid, component, args); + } + + // Show a popup when a pseudo-item falls asleep inside a bag. + private void OnTrySleeping(EntityUid uid, PseudoItemComponent component, TryingToSleepEvent args) + { + var parent = Transform(uid).ParentUid; + if (!HasComp(uid) && parent is { Valid: true } && HasComp(parent)) + _popup.PopupEntity(Loc.GetString("popup-sleep-in-bag", ("entity", uid)), uid); + } } diff --git a/Content.Server/Resist/CanEscapeInventoryComponent.cs b/Content.Server/Resist/CanEscapeInventoryComponent.cs index 19b4abf7d0c..978e03d95f9 100644 --- a/Content.Server/Resist/CanEscapeInventoryComponent.cs +++ b/Content.Server/Resist/CanEscapeInventoryComponent.cs @@ -15,4 +15,10 @@ public sealed partial class CanEscapeInventoryComponent : Component [DataField("doAfter")] public DoAfterId? DoAfter; + + /// + /// Action to cancel inventory escape. + /// + [DataField] + public EntityUid? EscapeCancelAction; } diff --git a/Content.Server/Resist/EscapeInventorySystem.cs b/Content.Server/Resist/EscapeInventorySystem.cs index 127db7d2b34..95a470e9093 100644 --- a/Content.Server/Resist/EscapeInventorySystem.cs +++ b/Content.Server/Resist/EscapeInventorySystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Server.Storage.Components; using Content.Shared.ActionBlocker; +using Content.Shared.Actions; using Content.Shared.DoAfter; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Events; @@ -13,6 +14,7 @@ using Content.Shared.Resist; using Content.Shared.Storage; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; namespace Content.Server.Resist; @@ -24,11 +26,17 @@ public sealed class EscapeInventorySystem : EntitySystem [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly CarryingSystem _carryingSystem = default!; // Carrying system from Nyanotrasen. + [Dependency] private readonly SharedActionsSystem _actions = default!; /// /// You can't escape the hands of an entity this many times more massive than you. /// public const float MaximumMassDisadvantage = 6f; + /// + /// Action to cancel inventory escape + /// + [ValidatePrototypeId] + private readonly string _escapeCancelAction = "ActionCancelEscape"; public override void Initialize() { @@ -37,6 +45,7 @@ public override void Initialize() SubscribeLocalEvent(OnRelayMovement); SubscribeLocalEvent(OnEscape); SubscribeLocalEvent(OnDropped); + SubscribeLocalEvent(OnCancelEscape); } private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent component, ref MoveInputEvent args) @@ -84,12 +93,20 @@ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent componen _popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting"), user, user); _popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting-target"), container, container); + + // Add an escape cancel action + if (component.EscapeCancelAction is not { Valid: true }) + _actions.AddAction(user, ref component.EscapeCancelAction, _escapeCancelAction); } private void OnEscape(EntityUid uid, CanEscapeInventoryComponent component, EscapeInventoryEvent args) { component.DoAfter = null; + // Remove the cancel action regardless of do-after result + _actions.RemoveAction(uid, component.EscapeCancelAction); + component.EscapeCancelAction = null; + if (args.Handled || args.Cancelled) return; @@ -109,4 +126,13 @@ private void OnDropped(EntityUid uid, CanEscapeInventoryComponent component, Dro if (component.DoAfter != null) _doAfterSystem.Cancel(component.DoAfter); } + + private void OnCancelEscape(EntityUid uid, CanEscapeInventoryComponent component, EscapeInventoryCancelActionEvent args) + { + if (component.DoAfter != null) + _doAfterSystem.Cancel(component.DoAfter); + + _actions.RemoveAction(uid, component.EscapeCancelAction); + component.EscapeCancelAction = null; + } } diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs new file mode 100644 index 00000000000..a28c7698fcd --- /dev/null +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Nyanotrasen.Item.PseudoItem; + +/// +/// Signifies that pseudo-item creatures can sleep inside the container to which this component is applied. +/// +[RegisterComponent] +public sealed partial class AllowsSleepInsideComponent : Component +{ +} diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs index d3774439d36..458b514b969 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs @@ -3,10 +3,10 @@ namespace Content.Shared.Nyanotrasen.Item.PseudoItem; - /// - /// For entities that behave like an item under certain conditions, - /// but not under most conditions. - /// +/// +/// For entities that behave like an item under certain conditions, +/// but not under most conditions. +/// [RegisterComponent, AutoGenerateComponentState] public sealed partial class PseudoItemComponent : Component { @@ -24,4 +24,10 @@ public sealed partial class PseudoItemComponent : Component public Vector2i StoredOffset; public bool Active = false; + + /// + /// Action for sleeping while inside a container with . + /// + [DataField] + public EntityUid? SleepAction; } diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs index 7000c654048..906503b3707 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs @@ -3,163 +3,33 @@ namespace Content.Shared.Nyanotrasen.Item.PseudoItem; -/// -/// Almost all of this is code taken from other systems, but adapted to use PseudoItem. -/// I couldn't use the original functions because the resolve would fuck shit up, even if I passed a constructed itemcomp -/// -/// This is horrible, and I hate it. But such is life -/// public partial class SharedPseudoItemSystem { - protected bool CheckItemFits(Entity itemEnt, Entity storageEnt) + /// + /// Checks if the pseudo-item can be inserted into the specified storage entity. + /// + /// + /// This function creates and uses a fake item component if the entity doesn't have one. + /// + public bool CheckItemFits(Entity itemEnt, Entity storageEnt) { if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp)) return false; - if (Transform(itemEnt).Anchored) + if (!TryComp(itemEnt, out var metadata)) return false; - if (storageEnt.Comp.Whitelist?.IsValid(itemEnt, EntityManager) == false) - return false; - - if (storageEnt.Comp.Blacklist?.IsValid(itemEnt, EntityManager) == true) - return false; - - var maxSize = _storage.GetMaxItemSize(storageEnt); - if (_item.GetSizePrototype(itemEnt.Comp.Size) > maxSize) - return false; - - // The following is shitfucked together straight from TryGetAvailableGridSpace, but eh, it works - - var itemComp = new ItemComponent - { Size = itemEnt.Comp.Size, Shape = itemEnt.Comp.Shape, StoredOffset = itemEnt.Comp.StoredOffset }; - - var storageBounding = storageEnt.Comp.Grid.GetBoundingBox(); - - Angle startAngle; - if (storageEnt.Comp.DefaultStorageOrientation == null) - startAngle = Angle.FromDegrees(-itemComp.StoredRotation); // PseudoItem doesn't support this - else - { - if (storageBounding.Width < storageBounding.Height) - { - startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Horizontal - ? Angle.Zero - : Angle.FromDegrees(90); - } - else - { - startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Vertical - ? Angle.Zero - : Angle.FromDegrees(90); - } - } - - for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++) - { - for (var x = storageBounding.Left; x <= storageBounding.Right; x++) - { - for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f) - { - var location = new ItemStorageLocation(angle, (x, y)); - if (ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation)) - return true; - } - } - } - - return false; - } - - private bool ItemFitsInGridLocation( - Entity itemEnt, - Entity storageEnt, - Vector2i position, - Angle rotation) - { - if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp)) - return false; - - var gridBounds = storageEnt.Comp.Grid.GetBoundingBox(); - if (!gridBounds.Contains(position)) - return false; - - var itemShape = GetAdjustedItemShape(itemEnt, rotation, position); - - foreach (var box in itemShape) + TryComp(itemEnt, out var item); + // If the entity doesn't have an item comp, create a fake one + // The fake component is never actually added to the entity + item ??= new ItemComponent { - for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++) - { - for (var offsetX = box.Left; offsetX <= box.Right; offsetX++) - { - var pos = (offsetX, offsetY); - - if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos, itemShape)) - return false; - } - } - } - - return true; - } - - private IReadOnlyList GetAdjustedItemShape(Entity entity, Angle rotation, - Vector2i position) - { - if (!Resolve(entity, ref entity.Comp)) - return new Box2i[] { }; - - var shapes = entity.Comp.Shape ?? _item.GetSizePrototype(entity.Comp.Size).DefaultShape; - var boundingShape = shapes.GetBoundingBox(); - var boundingCenter = ((Box2) boundingShape).Center; - var matty = Matrix3.CreateTransform(boundingCenter, rotation); - var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft; - - var adjustedShapes = new List(); - foreach (var shape in shapes) - { - var transformed = matty.TransformBox(shape).Translated(drift); - var floored = new Box2i(transformed.BottomLeft.Floored(), transformed.TopRight.Floored()); - var translated = floored.Translated(position); - - adjustedShapes.Add(translated); - } - - return adjustedShapes; - } - - private bool IsGridSpaceEmpty(Entity itemEnt, Entity storageEnt, - Vector2i location, IReadOnlyList shape) - { - if (!Resolve(storageEnt, ref storageEnt.Comp)) - return false; - - var validGrid = false; - foreach (var grid in storageEnt.Comp.Grid) - { - if (grid.Contains(location)) - { - validGrid = true; - break; - } - } - - if (!validGrid) - return false; - - foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems) - { - if (ent == itemEnt.Owner) - continue; - - var adjustedShape = shape; - foreach (var box in adjustedShape) - { - if (box.Contains(location)) - return false; - } - } + Owner = itemEnt, + Shape = itemEnt.Comp.Shape, + Size = itemEnt.Comp.Size, + StoredOffset = itemEnt.Comp.StoredOffset + }; - return true; + return _storage.CanInsert(storageEnt, itemEnt, out _, storageEnt.Comp, item, ignoreStacks: true); } } diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs index 4b7910746f1..5f4e6184346 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs @@ -1,14 +1,18 @@ +using Content.Shared.Actions; +using Content.Shared.Bed.Sleep; using Content.Shared.DoAfter; using Content.Shared.Hands; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; using Content.Shared.Item; using Content.Shared.Item.PseudoItem; +using Content.Shared.Popups; using Content.Shared.Storage; using Content.Shared.Storage.EntitySystems; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; namespace Content.Shared.Nyanotrasen.Item.PseudoItem; @@ -18,9 +22,13 @@ public abstract partial class SharedPseudoItemSystem : EntitySystem [Dependency] private readonly SharedItemSystem _item = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; [ValidatePrototypeId] private const string PreventTag = "PreventLabel"; + [ValidatePrototypeId] + private const string SleepActionId = "ActionSleep"; // The action used for sleeping inside bags. Currently uses the default sleep action (same as beds) public override void Initialize() { @@ -64,7 +72,7 @@ private void AddInsertVerb(EntityUid uid, PseudoItemComponent component, GetVerb args.Verbs.Add(verb); } - private bool TryInsert(EntityUid storageUid, EntityUid toInsert, PseudoItemComponent component, + public bool TryInsert(EntityUid storageUid, EntityUid toInsert, PseudoItemComponent component, StorageComponent? storage = null) { if (!Resolve(storageUid, ref storage)) @@ -87,6 +95,10 @@ private bool TryInsert(EntityUid storageUid, EntityUid toInsert, PseudoItemCompo return false; } + // If the storage allows sleeping inside, add the respective action + if (HasComp(storageUid)) + _actions.AddAction(toInsert, ref component.SleepAction, SleepActionId, toInsert); + component.Active = true; return true; } @@ -98,9 +110,11 @@ private void OnEntRemoved(EntityUid uid, PseudoItemComponent component, EntGotRe RemComp(uid); component.Active = false; + + _actions.RemoveAction(uid, component.SleepAction); // Remove sleep action if it was added } - private void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, + protected virtual void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args) { if (args.User == args.Item) @@ -154,7 +168,11 @@ protected void StartInsertDoAfter(EntityUid inserter, EntityUid toInsert, Entity NeedHand = true }; - _doAfter.TryStartDoAfter(args); + if (_doAfter.TryStartDoAfter(args)) + { + // Show a popup to the person getting picked up + _popupSystem.PopupEntity(Loc.GetString("carry-started", ("carrier", inserter)), toInsert, toInsert); + } } private void OnAttackAttempt(EntityUid uid, PseudoItemComponent component, AttackAttemptEvent args) diff --git a/Content.Shared/Resist/EscapeInventoryCancelEvent.cs b/Content.Shared/Resist/EscapeInventoryCancelEvent.cs new file mode 100644 index 00000000000..75ee09ff045 --- /dev/null +++ b/Content.Shared/Resist/EscapeInventoryCancelEvent.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Resist; + +public sealed partial class EscapeInventoryCancelActionEvent : InstantActionEvent; diff --git a/Resources/Locale/en-US/actions/actions/sleep.ftl b/Resources/Locale/en-US/actions/actions/sleep.ftl index fd833fd4a5c..6188e1639fe 100644 --- a/Resources/Locale/en-US/actions/actions/sleep.ftl +++ b/Resources/Locale/en-US/actions/actions/sleep.ftl @@ -5,3 +5,5 @@ sleep-examined = [color=lightblue]{CAPITALIZE(SUBJECT($target))} {CONJUGATE-BE($ wake-other-success = You shake {THE($target)} awake. wake-other-failure = You shake {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} not waking up. + +popup-sleep-in-bag = {THE($entity)} curls up and falls asleep. diff --git a/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl b/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl index 4fa1abae8bd..490daced3f2 100644 --- a/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl +++ b/Resources/Locale/en-US/nyanotrasen/carrying/carry.ftl @@ -1,3 +1,4 @@ carry-verb = Carry carry-too-heavy = You're not strong enough. +carry-started = {THE($carrier)} is trying to pick you up! diff --git a/Resources/Prototypes/Actions/misc.yml b/Resources/Prototypes/Actions/misc.yml new file mode 100644 index 00000000000..60fec699210 --- /dev/null +++ b/Resources/Prototypes/Actions/misc.yml @@ -0,0 +1,10 @@ +- type: entity + id: ActionCancelEscape + name: Stop escaping + description: Calm down and sit peacefuly in your carrier's inventory + noSpawn: true + components: + - type: InstantAction + icon: Actions/escapeinventory.rsi/cancel-escape.png + event: !type:EscapeInventoryCancelActionEvent + useDelay: 2 diff --git a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml index fbd5a02fa08..d72006f6c41 100644 --- a/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml +++ b/Resources/Prototypes/Entities/Clothing/Back/backpacks.yml @@ -30,6 +30,7 @@ delay: 0.5 - type: ExplosionResistance damageCoefficient: 0.9 + - type: AllowsSleepInside # DeltaV - enable sleeping inside bags - type: entity parent: ClothingBackpack @@ -258,7 +259,7 @@ - type: Sprite sprite: Clothing/Back/Backpacks/syndicate.rsi - type: ExplosionResistance - damageCoefficient: 0.1 + damageCoefficient: 0.1 #Special - type: entity diff --git a/Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png b/Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png new file mode 100644 index 0000000000000000000000000000000000000000..609e9e3d199149f20e44d2684167d14ec0353ee9 GIT binary patch literal 559 zcmV+~0?_@5P)C<~Jk;wDqh(aouDd;i};T#PBQ58a& zHyRDGDJ4mgYnmL_Nqgmd!})(FzRWWIEN{&&k(O4!}HkuiK@U*EcKAx@acl>3J9v x$Dd)UhGdTYLIJq2bd+Wom#K+8KmYF&_y*p|(%DtXaRmSX002ovPDHLkV1kRp{4oFk literal 0 HcmV?d00001 diff --git a/Resources/Textures/Actions/escapeinventory.rsi/meta.json b/Resources/Textures/Actions/escapeinventory.rsi/meta.json new file mode 100644 index 00000000000..ba379dedab4 --- /dev/null +++ b/Resources/Textures/Actions/escapeinventory.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Duffelbag icon taken from tgstation at commit https://github.com/tgstation/tgstation/commit/547852588166c8e091b441e4e67169e156bb09c1 | Modified into cancel-escape.png by Mnemotechnician (github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "cancel-escape" + } + ] +} From 00d786acd8eb2d76b05eb1560c68bdd479557693 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:29:07 +0000 Subject: [PATCH 04/21] Automatic Changelog Update (#484) --- Resources/Changelog/Changelog.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 847b9c90fbe..f0ee7cef2cc 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4293,3 +4293,17 @@ Entries: from being flunged into the void. id: 6137 time: '2024-07-05T16:53:25.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Fix + message: Carrying is less likely to behave erratically or suddenly interrupt now. + - type: Add + message: >- + You can now see when someone is trying to pick you up, and also you can + interrupt your attempt at escaping from their hands or inventory. + - type: Add + message: You can now properly take Felinids out of bags and place them inside. + - type: Add + message: Scientists have discovered that Felinids can sleep in bags. + id: 6138 + time: '2024-07-05T17:28:44.0000000+00:00' From 476e6ded461d9c1cfe0b9a4933ffe0880eca2e41 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:33:39 +0300 Subject: [PATCH 05/21] Port Paper Signatures (#456) # Description Ports delta-v paper signatures implemented by me in https://github.com/DeltaV-Station/Delta-v/pull/1172, including the changes later introduced by deltanedas in https://github.com/DeltaV-Station/Delta-v/pull/1345. Everything should be pretty self-explanatory, see the original PR for details. ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/50737402-a60d-425a-8938-f6e47427b22b) (see the original PR for a video demonstation)

--- # Changelog :cl: - add: You can now sign paper by alt-clicking it while holding a pen. --------- Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../DeltaV/Paper/SignAttemptEvent.cs | 8 ++ .../DeltaV/Paper/SignatureSystem.cs | 104 ++++++++++++++++++ .../Locale/en-US/deltav/paper/signature.ftl | 5 + .../Objects/Misc/bureaucracy.rsi/meta.json | 5 +- .../bureaucracy.rsi/paper_stamp-signature.png | Bin 0 -> 955 bytes 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 Content.Server/DeltaV/Paper/SignAttemptEvent.cs create mode 100644 Content.Server/DeltaV/Paper/SignatureSystem.cs create mode 100644 Resources/Locale/en-US/deltav/paper/signature.ftl create mode 100644 Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png diff --git a/Content.Server/DeltaV/Paper/SignAttemptEvent.cs b/Content.Server/DeltaV/Paper/SignAttemptEvent.cs new file mode 100644 index 00000000000..ff2d1b5103a --- /dev/null +++ b/Content.Server/DeltaV/Paper/SignAttemptEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.DeltaV.Paper; + +/// +/// Raised on the pen when trying to sign a paper. +/// If it's cancelled the signature wasn't made. +/// +[ByRefEvent] +public record struct SignAttemptEvent(EntityUid Paper, EntityUid User, bool Cancelled = false); diff --git a/Content.Server/DeltaV/Paper/SignatureSystem.cs b/Content.Server/DeltaV/Paper/SignatureSystem.cs new file mode 100644 index 00000000000..07a249399bc --- /dev/null +++ b/Content.Server/DeltaV/Paper/SignatureSystem.cs @@ -0,0 +1,104 @@ +using Content.Server.Access.Systems; +using Content.Server.Paper; +using Content.Server.Popups; +using Content.Shared.Paper; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Content.Shared.Verbs; +using Robust.Server.Audio; +using Robust.Shared.Player; + +namespace Content.Server.DeltaV.Paper; + +public sealed class SignatureSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly IdCardSystem _idCard = default!; + [Dependency] private readonly PaperSystem _paper = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly TagSystem _tags = default!; + + // The sprite used to visualize "signatures" on paper entities. + private const string SignatureStampState = "paper_stamp-signature"; + + public override void Initialize() + { + SubscribeLocalEvent>(OnGetAltVerbs); + } + + private void OnGetAltVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (args.Using is not {} pen || !_tags.HasTag(pen, "Write")) + return; + + var user = args.User; + AlternativeVerb verb = new() + { + Act = () => + { + TrySignPaper(ent, user, pen); + }, + Text = Loc.GetString("paper-sign-verb"), + DoContactInteraction = true, + Priority = 10 + }; + args.Verbs.Add(verb); + } + + /// + /// Tries add add a signature to the paper with signer's name. + /// + public bool TrySignPaper(Entity paper, EntityUid signer, EntityUid pen) + { + var comp = paper.Comp; + + var ev = new SignAttemptEvent(paper, signer); + RaiseLocalEvent(pen, ref ev); + if (ev.Cancelled) + return false; + + var signatureName = DetermineEntitySignature(signer); + + var stampInfo = new StampDisplayInfo() + { + StampedName = signatureName, + StampedColor = Color.DarkSlateGray, //TODO Make this configurable depending on the pen. + }; + + if (!comp.StampedBy.Contains(stampInfo) && _paper.TryStamp(paper, stampInfo, SignatureStampState, comp)) + { + // Show popups and play a paper writing sound + var signedOtherMessage = Loc.GetString("paper-signed-other", ("user", signer), ("target", paper.Owner)); + _popup.PopupEntity(signedOtherMessage, signer, Filter.PvsExcept(signer, entityManager: EntityManager), true); + + var signedSelfMessage = Loc.GetString("paper-signed-self", ("target", paper.Owner)); + _popup.PopupEntity(signedSelfMessage, signer, signer); + + _audio.PlayPvs(comp.Sound, signer); + + _paper.UpdateUserInterface(paper, comp); + + return true; + } + else + { + // Show an error popup + _popup.PopupEntity(Loc.GetString("paper-signed-failure", ("target", paper.Owner)), signer, signer, PopupType.SmallCaution); + + return false; + } + } + + private string DetermineEntitySignature(EntityUid uid) + { + // If the entity has an ID, use the name on it. + if (_idCard.TryFindIdCard(uid, out var id) && !string.IsNullOrWhiteSpace(id.Comp.FullName)) + return id.Comp.FullName; + + // Alternatively, return the entity name + return Name(uid); + } +} diff --git a/Resources/Locale/en-US/deltav/paper/signature.ftl b/Resources/Locale/en-US/deltav/paper/signature.ftl new file mode 100644 index 00000000000..87741c962c0 --- /dev/null +++ b/Resources/Locale/en-US/deltav/paper/signature.ftl @@ -0,0 +1,5 @@ +paper-sign-verb = Sign + +paper-signed-other = {CAPITALIZE(THE($user))} signs {THE($target)}. +paper-signed-self = You sign {THE($target)}. +paper-signed-failure = You cannot sign {THE($target)} diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json index 5117df77356..b57f9844bc7 100644 --- a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432. paper_stamp-syndicate by Veritius. paper_receipt, paper_receipt_horizontal by eoineoineoin. pen_centcom is a resprited version of pen_cap by PuroSlavKing (Github). Luxury pen is drawn by Ubaser. Lawyer and psychologist paper stamp resprited by Guess-My-Name", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432. paper_stamp-syndicate by Veritius. paper_receipt, paper_receipt_horizontal by eoineoineoin. pen_centcom is a resprited version of pen_cap by PuroSlavKing (Github). Luxury pen is drawn by Ubaser. Lawyer and psychologist paper stamp resprited by Guess-My-Name. paper_stamp-signature by Mnemotechnician.", "size": { "x": 32, "y": 32 @@ -259,6 +259,9 @@ }, { "name": "paper_stamp-psychologist" + }, + { + "name": "paper_stamp-signature" } ] } diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png b/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7aa083ee597521958a77c2e68c9adf01ef78f5 GIT binary patch literal 955 zcmZvaziU)M5XXmHV#09&3l%jYK?aEy62VrC-o<1vgcwpNq&TpiN)S9Hkf`8^)fWq| zR2wS`!2v53Erm1|!UXg`h=ri2IL>>4a6f#oyR(m-{e0&;+g@6n8?PR&64AKKH|rk4>Jc$6(Y(UfUBAaGFEjF%Rt0)PO0JM27^LHg@r8)4mA}B zDql8GyU1Yrj~-7{6X^~%pGQ#D5`k^?rJ7L#_FE; zXCUr5a}1~t3x}H;1e$sgu5Mz;Iy79|vOWpVx{K^ySUS_qO9OY2+1wp!N=a~uA8?L( zoy%=^DMgb5<_Z%9ixPXE*OWrL>?Squ%Me&7Tr_(CV(l%rcq0f?hgfz_NVCq~*C5o} zpA<LPcuJrImEY#cq2UZ}NaOsR literal 0 HcmV?d00001 From 692ceff845aa4e2a5a519d40d6d666cfa0d6a8f8 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:34:01 +0000 Subject: [PATCH 06/21] Automatic Changelog Update (#456) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f0ee7cef2cc..1b79764a611 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4307,3 +4307,9 @@ Entries: message: Scientists have discovered that Felinids can sleep in bags. id: 6138 time: '2024-07-05T17:28:44.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Add + message: You can now sign paper by alt-clicking it while holding a pen. + id: 6139 + time: '2024-07-05T17:33:39.0000000+00:00' From e06045ce586fa29f9404b8ae8f83f887dd5d8e81 Mon Sep 17 00:00:00 2001 From: WarMechanic <69510347+WarMechanic@users.noreply.github.com> Date: Sat, 6 Jul 2024 03:49:24 +1000 Subject: [PATCH 07/21] Felinid Soft Thieving + Trait (#500) # Description Reworks felinid thieving to no longer act like passive thieving gloves which grant invisible stripping (hereon referred to as hard stealing), in favour of soft thievery (hereon referred to as soft stealing). Soft thievery comprises of the following: - A smaller popup, with the thief anonymised. - A visible doafter bar - A 33% faster strip speed, that stacks with Thieving gloves - An additional ability to identify hidden items to better plan your course of action You no longer need to completely avoid felinids to maintain your precious items as long as you pay attention. For a felinid to utilise their thieving passive, they are encouraged to exploit any distractions to make moves on a target. If there is none, create one through conversation or other forms of player interaction. If you are suspected, persuade your victim that the thief is in fact, the other person. A faster strip speed makes thief bonuses diegetic to other players, and also improves the value proposition of thieving gloves on someone who already has thieving bonuses. Any other race can also gain soft thievery via a moderate costing trait. Non-felinid thieves are encouraged to exploit any felinids as a scapegoat. --- # TODO Code - [X] IgnoreStripHidden - allows thieves to look into peoples pockets - [X] StripTimeMultiplier - stripping at a multiplicative rate helps strip bags/belts which creates trait value - [X] Stealthy > Stealth - rather than a bool, distinguishes stealth levels as an enum Balance - [X] Soft thieves can identify items in pockets, which creates player agency - [X] Soft thieves steal 33% faster, which stacks with thieving gloves - [X] Victims to soft stealing get a smaller popup, useful if they're preoccupied - [X] Soft thievery is a trait, which Felinids get for free - [X] Felinids no longer hard steal items Media - [x] Attach media ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69510347/c0c6e81f-21e2-48c5-a535-777c1f683ef7) ![video](https://www.youtube.com/embed/elDfOgAPmIs?si=yV5JjOaSYvurGZer)

--- # Changelog :cl: - add: Added the Thievery trait, which provides various soft stripping bonuses - tweak: Felinids no longer have passive thieving gloves, they instead get the Thievery trait by default --------- Signed-off-by: WarMechanic <69510347+WarMechanic@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Inventory/StrippableBoundUserInterface.cs | 5 +- Content.Server/Strip/StrippableSystem.cs | 51 +++++++++++-------- .../EntitySystems/ToggleableClothingSystem.cs | 10 ++-- .../Strip/Components/StrippableComponent.cs | 8 +-- .../Strip/Components/ThievingComponent.cs | 20 ++++++-- .../Strip/SharedStrippableSystem.cs | 18 ++++++- Content.Shared/Strip/ThievingSystem.cs | 39 +++++++++++++- .../en-US/strip/strippable-component.ftl | 6 ++- Resources/Locale/en-US/traits/traits.ftl | 5 ++ .../Entities/Clothing/Hands/gloves.yml | 2 - .../Entities/Clothing/Hands/specific.yml | 1 - .../Entities/Mobs/Player/admin_ghost.yml | 4 +- .../Entities/Mobs/Species/felinid.yml | 5 ++ Resources/Prototypes/Traits/skills.yml | 14 +++++ 14 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 Resources/Prototypes/Traits/skills.yml diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index f8eb12df914..4bb49fecc14 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -19,6 +19,7 @@ using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.Player; using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -31,6 +32,7 @@ namespace Content.Client.Inventory public sealed class StrippableBoundUserInterface : BoundUserInterface { [Dependency] private readonly IUserInterfaceManager _ui = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; private readonly ExamineSystem _examine; private readonly InventorySystem _inv; private readonly SharedCuffableSystem _cuffable; @@ -198,7 +200,8 @@ private void AddInventoryButton(EntityUid invUid, string slotId, InventoryCompon var entity = container.ContainedEntity; // If this is a full pocket, obscure the real entity - if (entity != null && slotDef.StripHidden) + if (entity != null && slotDef.StripHidden + && !(EntMan.TryGetComponent(_playerManager.LocalEntity, out var thiefcomponent) && thiefcomponent.IgnoreStripHidden)) entity = _virtualHiddenEntity; var button = new SlotButton(new SlotData(slotDef, container)); diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 3b38b65a19d..686570f7dca 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -36,6 +36,7 @@ public sealed class StrippableSystem : SharedStrippableSystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; // TODO: ECS popups. Not all of these have ECS equivalents yet. @@ -251,15 +252,17 @@ private void StartStripInsertInventory( var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - if (!stealth) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large); + bool hidden = stealth == ThievingStealth.Hidden; - var prefix = stealth ? "stealthily " : ""; + if (!hidden) + StripPopup("strippable-component-alert-owner-insert", stealth, target, user: Identity.Entity(user, EntityManager), item: user.Comp.ActiveHandEntity!.Value); + + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -340,20 +343,22 @@ private void StartStripRemoveInventory( var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - if (!stealth) + bool hidden = stealth == ThievingStealth.Hidden; + + if (!hidden) { if (slotDef.StripHidden) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large); + StripPopup("strippable-component-alert-owner-hidden", stealth, target, slot: slot); else - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large); + StripPopup("strippable-component-alert-owner", stealth, target, user: Identity.Entity(user, EntityManager), item: item); } - var prefix = stealth ? "stealthily " : ""; + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -374,7 +379,7 @@ private void StripRemoveInventory( EntityUid target, EntityUid item, string slot, - bool stealth) + bool hidden) { if (!CanStripRemoveInventory(user, target, item, slot)) return; @@ -384,7 +389,7 @@ private void StripRemoveInventory( RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc. - _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth); + _handsSystem.PickupOrDrop(user, item, animateUser: hidden, animate: hidden); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); } @@ -446,12 +451,14 @@ private void StartStripInsertHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - var prefix = stealth ? "stealthily " : ""; + bool hidden = stealth == ThievingStealth.Hidden; + + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -471,7 +478,7 @@ private void StripInsertHand( Entity target, EntityUid held, string handName, - bool stealth) + bool hidden) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) @@ -481,7 +488,7 @@ private void StripInsertHand( return; _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp); - _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp); + _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: hidden, animate: hidden, handsComp: target.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); // Hand update will trigger strippable update. @@ -543,15 +550,17 @@ private void StartStripRemoveHand( var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay); - if (!stealth) - _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target); + bool hidden = stealth == ThievingStealth.Hidden; + + if (!hidden) + StripPopup("strippable-component-alert-owner", stealth, target, user: Identity.Entity(user, EntityManager), item: item); - var prefix = stealth ? "stealthily " : ""; + var prefix = hidden ? "stealthily " : ""; _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item) { - Hidden = stealth, + Hidden = hidden, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -572,7 +581,7 @@ private void StripRemoveHand( Entity target, EntityUid item, string handName, - bool stealth) + bool hidden) { if (!Resolve(user, ref user.Comp) || !Resolve(target, ref target.Comp)) @@ -582,7 +591,7 @@ private void StripRemoveHand( return; _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp); - _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp); + _handsSystem.PickupOrDrop(user, item, animateUser: hidden, animate: hidden, handsComp: user.Comp); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); // Hand update will trigger strippable update. diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 22a1d1a8f52..4abe7bc876a 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -27,6 +27,7 @@ public sealed class ToggleableClothingSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedStrippableSystem _strippable = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; public override void Initialize() { @@ -97,6 +98,8 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value); + bool hidden = (stealth == ThievingStealth.Hidden); + var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { BreakOnDamage = true, @@ -110,11 +113,8 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg if (!_doAfter.TryStartDoAfter(args)) return; - if (!stealth) - { - var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", item)); - _popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large); - } + if (!hidden) + _strippable.StripPopup("strippable-component-alert-owner-interact", stealth, wearer, user: Identity.Entity(user, EntityManager), item: item); } private void OnGetAttachedStripVerbsEvent(EntityUid uid, AttachedClothingComponent component, GetVerbsEvent args) diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index 4faca4d8f21..00725808297 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -32,12 +32,12 @@ public sealed class StrippingSlotButtonPressed(string slot, bool isHand) : Bound public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage; [ByRefEvent] - public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent + public abstract class BaseBeforeStripEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : EntityEventArgs, IInventoryRelayEvent { public readonly TimeSpan InitialTime = initialTime; public float Multiplier = 1f; public TimeSpan Additive = TimeSpan.Zero; - public bool Stealth = stealth; + public ThievingStealth Stealth = stealth; public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier + Additive.Seconds, 0f)); @@ -51,7 +51,7 @@ public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// [ByRefEvent] - public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + public sealed class BeforeStripEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : BaseBeforeStripEvent(initialTime, stealth); /// /// Used to modify strip times. Raised directed at the target. @@ -60,7 +60,7 @@ public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. /// [ByRefEvent] - public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, ThievingStealth stealth = ThievingStealth.Obvious) : BaseBeforeStripEvent(initialTime, stealth); /// /// Organizes the behavior of DoAfters for . diff --git a/Content.Shared/Strip/Components/ThievingComponent.cs b/Content.Shared/Strip/Components/ThievingComponent.cs index a851dd5ef63..1d584627727 100644 --- a/Content.Shared/Strip/Components/ThievingComponent.cs +++ b/Content.Shared/Strip/Components/ThievingComponent.cs @@ -9,14 +9,24 @@ public sealed partial class ThievingComponent : Component /// /// How much the strip time should be shortened by /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stripTimeReduction")] + [DataField] public TimeSpan StripTimeReduction = TimeSpan.FromSeconds(0.5f); + /// + /// A multiplier coefficient for strip time + /// + [DataField] + public float StripTimeMultiplier = 1f; + /// /// Should it notify the user if they're stripping a pocket? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stealthy")] - public bool Stealthy; + [DataField] + public ThievingStealth Stealth = ThievingStealth.Hidden; + + /// + /// Should the user be able to see hidden items? (i.e pockets) + /// + [DataField] + public bool IgnoreStripHidden; } diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index 7afd4f245a1..64dd6a81f3a 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -1,11 +1,14 @@ using Content.Shared.DragDrop; using Content.Shared.Hands.Components; +using Content.Shared.Popups; using Content.Shared.Strip.Components; namespace Content.Shared.Strip; public abstract class SharedStrippableSystem : EntitySystem { + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ThievingSystem _thieving = default!; public override void Initialize() { base.Initialize(); @@ -14,7 +17,7 @@ public override void Initialize() SubscribeLocalEvent(OnDragDrop); } - public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) + public (TimeSpan Time, ThievingStealth Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) { var userEv = new BeforeStripEvent(initialTime); RaiseLocalEvent(user, ref userEv); @@ -55,4 +58,17 @@ private void OnCanDrop(EntityUid uid, StrippableComponent component, ref CanDrop if (args.CanDrop) args.Handled = true; } + + public void StripPopup(string messageId, ThievingStealth stealth, EntityUid target, EntityUid? user = null, EntityUid? item = null, string slot = "") + { + bool subtle = stealth == ThievingStealth.Subtle; + PopupType? popupSize = _thieving.GetPopupTypeFromStealth(stealth); + + if (popupSize.HasValue) // We should always have a value if we're not hidden + _popup.PopupEntity(Loc.GetString(messageId, + ("user", subtle ? Loc.GetString("thieving-component-user") : user ?? EntityUid.Invalid), + ("item", subtle ? Loc.GetString("thieving-component-item") : item ?? EntityUid.Invalid), + ("slot", slot)), + target, target, popupSize.Value); + } } diff --git a/Content.Shared/Strip/ThievingSystem.cs b/Content.Shared/Strip/ThievingSystem.cs index 2b3d3b38a00..8f523accfea 100644 --- a/Content.Shared/Strip/ThievingSystem.cs +++ b/Content.Shared/Strip/ThievingSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Inventory; -using Content.Shared.Strip; +using Content.Shared.Popups; using Content.Shared.Strip.Components; +using Robust.Shared.Serialization; namespace Content.Shared.Strip; @@ -17,7 +18,41 @@ public override void Initialize() private void OnBeforeStrip(EntityUid uid, ThievingComponent component, BeforeStripEvent args) { - args.Stealth |= component.Stealthy; + args.Stealth = (ThievingStealth) Math.Max((sbyte) args.Stealth, (sbyte) component.Stealth); args.Additive -= component.StripTimeReduction; + args.Multiplier *= component.StripTimeMultiplier; } + + public PopupType? GetPopupTypeFromStealth(ThievingStealth stealth) + { + switch (stealth) + { + case ThievingStealth.Hidden: + return null; + + case ThievingStealth.Subtle: + return PopupType.Small; + + default: + return PopupType.Large; + } + } +} +[Serializable, NetSerializable] +public enum ThievingStealth : sbyte +{ + /// + /// Target sees a large popup indicating that an item is being stolen by who + /// + Obvious = 0, + + /// + /// Target sees a small popup indicating that an item is being stolen + /// + Subtle = 1, + + /// + /// Target does not see any popup regarding the stealing of an item + /// + Hidden = 2 } diff --git a/Resources/Locale/en-US/strip/strippable-component.ftl b/Resources/Locale/en-US/strip/strippable-component.ftl index 7654b20b03f..65d7844ee22 100644 --- a/Resources/Locale/en-US/strip/strippable-component.ftl +++ b/Resources/Locale/en-US/strip/strippable-component.ftl @@ -19,4 +19,8 @@ strip-verb-get-data-text = Strip ## UI strippable-bound-user-interface-stripping-menu-title = {$ownerName}'s inventory -strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints \ No newline at end of file +strippable-bound-user-interface-stripping-menu-ensnare-button = Remove Leg Restraints + +# Stealth +thieving-component-user = Someone +thieving-component-item = something \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index e9163bdb548..80680ac0db2 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -31,3 +31,8 @@ trait-description-SocialAnxiety = You are anxious when you speak and stutter. trait-name-Snoring = Snoring trait-description-Snoring = You will snore while sleeping. + +trait-name-Thieving = Thieving +trait-description-Thieving = + You are deft with your hands, and talented at convincing people of their belongings. + You can identify pocketed items, steal them quieter, and steal ~33% faster. diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index bf08db78f71..4cd0c04e2be 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -231,7 +231,6 @@ - type: FingerprintMask - type: Thieving stripTimeReduction: 1 - stealthy: true - type: NinjaGloves - type: entity @@ -332,7 +331,6 @@ tags: [] # ignore "WhitelistChameleon" tag - type: Thieving stripTimeReduction: 1.5 - stealthy: true - type: entity parent: ClothingHandsGlovesColorWhite diff --git a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml index e6a57319999..db34297b42a 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/specific.yml @@ -29,4 +29,3 @@ components: - type: Thieving stripTimeReduction: 2 - stealthy: true diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 80e87d3670c..9bdfb18830e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -85,8 +85,8 @@ range: 500 - type: StationLimitedNetwork - type: Thieving - stripTimeReduction: 9999 - stealthy: true + stripTimeMultiplier: 0 + ignoreStripHidden: true - type: Stripping - type: SolutionScanner - type: IgnoreUIRange diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index 5bc02461eed..d23607b16d5 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -74,6 +74,11 @@ - GalacticCommon - SolCommon - Nekomimetic + - type: Thieving + ignoreStripHidden: true + stealth: Subtle + stripTimeReduction: 0 + stripTimeMultiplier: 0.667 - type: entity save: false diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml new file mode 100644 index 00000000000..6175834c1fc --- /dev/null +++ b/Resources/Prototypes/Traits/skills.yml @@ -0,0 +1,14 @@ +- type: trait + id: Thieving + category: Physical + points: -4 + components: + - type: Thieving + ignoreStripHidden: true + stealth: Subtle + stripTimeReduction: 0 + stripTimeMultiplier: 0.667 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Felinid From 4609f9e9b0dae53d99f4aeb46f20cbfbc2206b46 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:49:57 +0300 Subject: [PATCH 08/21] Cherry-Pick Carrypets From Delta-V (#501) # Description Cherry-picks https://github.com/DeltaV-Station/Delta-v/pull/1145 All credit goes to the original author of the PR. Original description is: "adds carriable component to a lot of animals that didn't have it" --- # Why ## Renault my beloved!!! ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/12777e9b-7d00-4df2-8703-a7f9e42ea1c6)

--- # Changelog :cl: Froffy025 - add: You can now carry most of the station pets. --------- Signed-off-by: Froffy025 <78222136+Froffy025@users.noreply.github.com> Co-authored-by: Froffy025 <78222136+froffy025@users.noreply.github.com> --- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 30 +++++++++++++++++++ .../Prototypes/Entities/Mobs/NPCs/carp.yml | 1 + .../Entities/Mobs/NPCs/regalrat.yml | 2 ++ .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 3 +- .../Prototypes/Entities/Mobs/NPCs/space.yml | 2 +- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index e311681ce5f..7fe105f940c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -13,6 +13,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: bat sprite: Mobs/Animals/bat.rsi + - type: Carriable - type: Speech speechSounds: Squeak speechVerb: SmallMob @@ -188,6 +189,8 @@ noMovementLayers: movement: state: chicken-0 + - type: Carriable + freeHandsRequired: 1 - type: Fixtures fixtures: fix1: @@ -580,6 +583,8 @@ - MobMask layer: - MobLayer + - type: Carriable + freeHandsRequired: 1 - type: Tag tags: - DoorBumpOpener @@ -840,6 +845,8 @@ noMovementLayers: movement: state: crab + - type: Carriable + freeHandsRequired: 1 - type: Physics - type: Fixtures fixtures: @@ -907,6 +914,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: goat sprite: Mobs/Animals/goat.rsi + - type: Carriable - type: Fixtures fixtures: fix1: @@ -999,6 +1007,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: goose sprite: Mobs/Animals/goose.rsi + - type: Carriable - type: Fixtures fixtures: fix1: @@ -1245,6 +1254,7 @@ sprite: "Effects/creampie.rsi" state: "creampie_human" visible: false + - type: Carriable - type: Hands - type: GenericVisualizer visuals: @@ -1767,6 +1777,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: lizard sprite: Mobs/Animals/lizard.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -1821,6 +1832,8 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: slug sprite: Mobs/Animals/slug.rsi + - type: Carriable + freeHandsRequired: 1 - type: Physics - type: Fixtures fixtures: @@ -1873,6 +1886,7 @@ noMovementLayers: movement: state: frog + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -1936,6 +1950,8 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: parrot sprite: Mobs/Animals/parrot.rsi + - type: Carriable + freeHandsRequired: 1 - type: Fixtures fixtures: fix1: @@ -1991,6 +2007,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: penguin sprite: Mobs/Animals/penguin.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2124,6 +2141,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: snake sprite: Mobs/Animals/snake.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2186,6 +2204,7 @@ noMovementLayers: movement: state: tarantula + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2371,6 +2390,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: possum + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2446,6 +2466,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: raccoon + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2510,6 +2531,7 @@ noMovementLayers: movement: state: fox + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2590,6 +2612,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: corgi + - type: Carriable - type: Physics - type: Speech speechVerb: Canine @@ -2746,6 +2769,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: cat + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -2925,6 +2949,8 @@ Base: kitten_dead Dead: Base: kitten_dead + - type: Carriable + freeHandsRequired: 1 - type: Butcherable spawned: - id: FoodMeat @@ -2955,6 +2981,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: sloth + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -3016,6 +3043,7 @@ layers: - map: ["enum.DamageStateVisualLayers.Base"] state: ferret + - type: Carriable - type: Physics - type: Fixtures fixtures: @@ -3217,6 +3245,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: pig sprite: Mobs/Animals/pig.rsi + - type: Carriable - type: Fixtures fixtures: fix1: @@ -3292,6 +3321,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: nymph sprite: Mobs/Animals/nymph.rsi + - type: Carriable - type: Physics - type: Fixtures fixtures: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 3e6c603626b..2aae27d31ef 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -22,6 +22,7 @@ layers: - map: [ "enum.DamageStateVisualLayers.Base" ] state: alive + - type: Carriable # This one is for you, deltanedas o7 - type: CombatMode - type: Physics - type: Fixtures diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 50fe3b6765e..31a32333f3f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -170,6 +170,8 @@ description: He's da mini rat. He don't make da roolz. noSpawn: true #Must be configured to a King or the AI breaks. components: + - type: Carriable + freeHandsRequired: 1 - type: CombatMode - type: MovementSpeedModifier baseWalkSpeed : 3.5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 901bf149cbc..f18b371c4c2 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity name: basic slime id: MobAdultSlimes parent: [ SimpleMobBase, MobCombat ] @@ -17,6 +17,7 @@ layers: - map: [ "enum.DamageStateVisualLayers.Base" ] state: blue_adult_slime + - type: Carriable - type: Fixtures fixtures: fix1: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 9ea2d784dbb..9b79d67f408 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -1,4 +1,4 @@ -- type: entity +- type: entity name: basic id: MobSpaceBasic parent: SimpleSpaceMobBase From 4ebb3cc779b9df30d877f680b44dc287c22f1d19 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:53:28 +0000 Subject: [PATCH 09/21] Automatic Changelog Update (#500) --- Resources/Changelog/Changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1b79764a611..618c03ec9ed 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4313,3 +4313,13 @@ Entries: message: You can now sign paper by alt-clicking it while holding a pen. id: 6139 time: '2024-07-05T17:33:39.0000000+00:00' +- author: WarMechanic + changes: + - type: Add + message: Added the Thievery trait, which provides various soft stripping bonuses + - type: Tweak + message: >- + Felinids no longer have passive thieving gloves, they instead get the + Thievery trait by default + id: 6140 + time: '2024-07-05T17:49:25.0000000+00:00' From d971e7c5119ce3fa57339f15d47fbbf372a0e3b5 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 17:54:28 +0000 Subject: [PATCH 10/21] Automatic Changelog Update (#501) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 618c03ec9ed..d602db8a69b 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4323,3 +4323,9 @@ Entries: Thievery trait by default id: 6140 time: '2024-07-05T17:49:25.0000000+00:00' +- author: Froffy025 + changes: + - type: Add + message: You can now carry most of the station pets. + id: 6141 + time: '2024-07-05T17:49:57.0000000+00:00' From d5f73ad370982fa57b223099e016a93f033fd378 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:10:24 +0300 Subject: [PATCH 11/21] Fix Them Pesky Job Requirements (#511) # Description Fixes job requirements using wrong locales Happened because one function had role-timer- as its default locale prefix and the other that called the first had null as the default. ---

Media

![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/0d02c40a-a58f-4b48-89f5-e0a6b4ff75b9) ![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/f3b651dd-492e-47c0-aa9f-74998c78f792)

--- # Changelog :cl: - fix: Job requirements are now displayed correctly. --- .../Players/PlayTimeTracking/JobRequirementsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs index 7688a3b3aaa..ee581186f35 100644 --- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs +++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs @@ -96,7 +96,7 @@ public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessag return CheckRoleTime(job.Requirements, out reason); } - public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(false)] out FormattedMessage? reason, string? localePrefix = null) + public bool CheckRoleTime(HashSet? requirements, [NotNullWhen(false)] out FormattedMessage? reason, string? localePrefix = "role-timer-") { reason = null; From c4f77007441d8d7fef9067d4d6f70ec7182c3657 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 19:10:53 +0000 Subject: [PATCH 12/21] Automatic Changelog Update (#511) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d602db8a69b..f9c1de93996 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4329,3 +4329,9 @@ Entries: message: You can now carry most of the station pets. id: 6141 time: '2024-07-05T17:49:57.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Fix + message: Job requirements are now displayed correctly. + id: 6142 + time: '2024-07-05T19:10:24.0000000+00:00' From aaee45aa0aaa063f9d2044d186f1f5a0966d58e9 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:03:28 +0300 Subject: [PATCH 13/21] Restore Old Event Rates (#509) # Description This restores the basic/survival event rates to how they were before before #486 (however, it keeps the CVars created in it). - 5-25 minutes for basic instead of 15-35 - 4-12 minutes for survival instead of 20-45 --- # Why The PR made it so that survival rounds actually have less events than extended - that is considering that survival rounds are supposed to be troublesome and filled with events while extended ones are supposed to be a chill alternative. Other forks don't bother with editing CVars every 5 minutes, so this "opportunity to configure their experience" was never addressed. Based on the feedback I've received from other players, the change was rather negative. From my personal experience, survival rounds became an extended extended, where during a 2-hour shift you can get a grand total of one power outage and two mice infestations, and nothing else at all. --- # Changelog :cl: - tweak: Events should now occur as frequently as before. Note: server owner can configure the frequency on their server manually. Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> --- Content.Shared/CCVar/CCVars.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 7bde756f7c3..3961818baaa 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -112,14 +112,14 @@ public static readonly CVarDef /// Close to how long you expect a round to last, so you'll probably have to tweak this on downstreams. ///
public static readonly CVarDef - EventsRampingAverageEndTime = CVarDef.Create("events.ramping_average_end_time", 120f, CVar.ARCHIVE | CVar.SERVERONLY); + EventsRampingAverageEndTime = CVarDef.Create("events.ramping_average_end_time", 40f, CVar.ARCHIVE | CVar.SERVERONLY); /// /// Average ending chaos modifier for the ramping event scheduler. /// Max chaos chosen for a round will deviate from this /// public static readonly CVarDef - EventsRampingAverageChaos = CVarDef.Create("events.ramping_average_chaos", 4f, CVar.ARCHIVE | CVar.SERVERONLY); + EventsRampingAverageChaos = CVarDef.Create("events.ramping_average_chaos", 6f, CVar.ARCHIVE | CVar.SERVERONLY); /* * Game @@ -176,26 +176,26 @@ public static readonly CVarDef /// /// Minimum time between Basic station events in seconds /// - public static readonly CVarDef // 15 Minutes - GameEventsBasicMinimumTime = CVarDef.Create("game.events_basic_minimum_time", 900, CVar.SERVERONLY); + public static readonly CVarDef // 5 Minutes + GameEventsBasicMinimumTime = CVarDef.Create("game.events_basic_minimum_time", 300, CVar.SERVERONLY); /// /// Maximum time between Basic station events in seconds /// - public static readonly CVarDef // 35 Minutes - GameEventsBasicMaximumTime = CVarDef.Create("game.events_basic_maximum_time", 2100, CVar.SERVERONLY); + public static readonly CVarDef // 25 Minutes + GameEventsBasicMaximumTime = CVarDef.Create("game.events_basic_maximum_time", 1500, CVar.SERVERONLY); /// /// Minimum time between Ramping station events in seconds /// - public static readonly CVarDef // 20 Minutes - GameEventsRampingMinimumTime = CVarDef.Create("game.events_ramping_minimum_time", 1200, CVar.SERVERONLY); + public static readonly CVarDef // 4 Minutes + GameEventsRampingMinimumTime = CVarDef.Create("game.events_ramping_minimum_time", 240, CVar.SERVERONLY); /// /// Maximum time between Ramping station events in seconds /// - public static readonly CVarDef // 45 Minutes - GameEventsRampingMaximumTime = CVarDef.Create("game.events_ramping_maximum_time", 2700, CVar.SERVERONLY); + public static readonly CVarDef // 12 Minutes + GameEventsRampingMaximumTime = CVarDef.Create("game.events_ramping_maximum_time", 720, CVar.SERVERONLY); /// /// From 50092e9988ec1718272a72e54507765eb81b61dc Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 21:03:51 +0000 Subject: [PATCH 14/21] Automatic Changelog Update (#509) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f9c1de93996..cc18a7b1e00 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4335,3 +4335,11 @@ Entries: message: Job requirements are now displayed correctly. id: 6142 time: '2024-07-05T19:10:24.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Tweak + message: >- + Events should now occur as frequently as before. Note: server owner can + configure the frequency on their server manually. + id: 6143 + time: '2024-07-05T21:03:28.0000000+00:00' From 8de45e767723a08d824bba4c2fda8411f2081e02 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 5 Jul 2024 18:53:36 -0400 Subject: [PATCH 15/21] More Loadout Items Wave 1 (#425) # Description The original release of Loadouts to EE did not have much in the way of actual implementation as far as the item list was concerned. This PR seeks to get a head start at fixing that by implementing a fairly sizeable chunk of new items that can be bought with loadout points. One important addition is the use of "non-clothing-job-related items", which helps further set this version of Loadouts apart from the /tg/ style of loadouts implemented elsewhere. One of the key strengths of this loadout system is that it's not limited to just replacing a job's clothing selections, but can also add items unrelated to clothing. My given example for this is the new option for Security characters(provided they have sufficient number of hours as a Security Officer) can spend roughly half their loadout points to start with a spare magazine for their handgun. Secondly, we also now have an opportunity to place non-nanotrasen corporate items in the game, in such a way that players can tie their characters to other corporations. Ostensibly this is to build up to the idea of "Contractor" characters. I hope that this will help broaden people's horizons. # On "Syndie" items I'd like to take this as an opportunity to kind of push people away from the whole thing of "THATS A SYNDICATE ITEM!!!!" wherever possible. For the most part this is very much NRP behavior that I'd like to discourage. With some notable exceptions, this doesn't mean outright antag items should be obtainable through Loadouts. To that end, items such as Interdyne branded smokes, Cybersun branded clothing, and other items with "In-Universe Company Brands" should be moved away from their identity as strictly antag objects, and instead things characters could reasonably have bought with their paycheck. # Technical Changes I had to make some improvements to the way Loadouts handles species, namely it wasn't previously possible to have an item require any entry from a list of species. I updated it so that SpeciesRequirement takes a list of Species, and will accept any entry from that list. My given example for this is the entries for Nitrogen air tanks, which can only be purchased in loadouts by SlimePersons and Vox(who are not currently playable, but I am accounting for them). Other usage of this is in the Oni-specific Security items. I do hope to use this as an opportunity to introduce more species-specific goods in the future. # Media Outerwear ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/8d0d9286-7758-45f3-aaff-506dddd35da6) More job-related selections available to some departments ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/7b214593-7111-42d7-966e-2a340c766f61) More variety of trinkets and items, including instruments! ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/1b7befaf-c94c-4238-b93b-cbdf14e939dd) Species Specific Items! ![image](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/e4a2a7e6-085d-4e1b-9ed9-635af8b70e84) # Changelog :cl: VMSolidus - add: The catalog of items available via Loadouts has been greatly expanded. Have fun customizing your characters! --------- Signed-off-by: VMSolidus Signed-off-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> Co-authored-by: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: SimpleStation Changelogs Co-authored-by: Pspritechologist <81725545+Pspritechologist@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> Co-authored-by: stellar-novas --- .../customization/character-requirements.ftl | 2 +- .../Locale/en-US/loadouts/categories.ftl | 7 + Resources/Locale/en-US/loadouts/items.ftl | 2 + .../Entities/Clothing/Belt/belts.yml | 14 + .../Smokeables/Cigarettes/cigarette.yml | 2 +- .../Smokeables/Cigarettes/packs.yml | 2 +- .../Prototypes/Loadouts/Jobs/medical.yml | 264 +++++++++++++++++ .../Prototypes/Loadouts/Jobs/science.yml | 56 ++++ .../Prototypes/Loadouts/Jobs/security.yml | 266 +++++++++++++++++- .../Prototypes/Loadouts/Jobs/service.yml | 17 ++ Resources/Prototypes/Loadouts/categories.yml | 17 +- Resources/Prototypes/Loadouts/eyes.yml | 12 +- Resources/Prototypes/Loadouts/hands.yml | 115 ++++++++ Resources/Prototypes/Loadouts/head.yml | 184 ++++++++++-- Resources/Prototypes/Loadouts/items.yml | 140 ++++++++- Resources/Prototypes/Loadouts/neck.yml | 85 +++++- .../Prototypes/Loadouts/outerClothing.yml | 73 ++++- Resources/Prototypes/Loadouts/shoes.yml | 94 ++++++- Resources/Prototypes/Loadouts/species.yml | 35 +++ Resources/Prototypes/Loadouts/uniform.yml | 240 +++++++++++++++- 20 files changed, 1563 insertions(+), 64 deletions(-) create mode 100644 Resources/Prototypes/Loadouts/hands.yml create mode 100644 Resources/Prototypes/Loadouts/species.yml diff --git a/Resources/Locale/en-US/customization/character-requirements.ftl b/Resources/Locale/en-US/customization/character-requirements.ftl index b073bdb773f..efa1b7e7677 100644 --- a/Resources/Locale/en-US/customization/character-requirements.ftl +++ b/Resources/Locale/en-US/customization/character-requirements.ftl @@ -9,7 +9,7 @@ character-species-requirement = You must {$inverted -> character-trait-requirement = You must {$inverted -> [true] not have *[other] have -} the trait [color=lightblue]{$trait}[/color] +} one of these traits: [color=lightblue]{$traits}[/color] character-backpack-type-requirement = You must {$inverted -> [true] not use *[other] use diff --git a/Resources/Locale/en-US/loadouts/categories.ftl b/Resources/Locale/en-US/loadouts/categories.ftl index 685c5cbcbd9..9770bd8bafd 100644 --- a/Resources/Locale/en-US/loadouts/categories.ftl +++ b/Resources/Locale/en-US/loadouts/categories.ftl @@ -2,7 +2,14 @@ loadout-category-Uncategorized = Uncategorized loadout-category-Accessories = Accessories +loadout-category-Eyes = Eyes +loadout-category-Hands = Hands +loadout-category-Head = Head loadout-category-Items = Items loadout-category-Jobs = Jobs +loadout-category-Mask = Mask +loadout-category-Neck = Neck loadout-category-Outer = Outer +loadout-category-Shoes = Shoes +loadout-category-Species = Species loadout-category-Uniform = Uniform diff --git a/Resources/Locale/en-US/loadouts/items.ftl b/Resources/Locale/en-US/loadouts/items.ftl index a4819011262..b92f56bc7cb 100644 --- a/Resources/Locale/en-US/loadouts/items.ftl +++ b/Resources/Locale/en-US/loadouts/items.ftl @@ -11,3 +11,5 @@ loadout-description-LoadoutItemPlushieSharkBlue = Dive into battle with your ver loadout-description-LoadoutItemPlushieSharkPink = Unleash the power of pink with the Pink Shark Plushie! This rosy-hued predator might not have real teeth, but its sheer adorableness is enough to take a bite out of anyone's resolve. Watch as foes melt away in the face of its cottony charm. loadout-description-LoadoutItemPlushieSharkGrey = Introducing the Grey Shark Plushie, the apex predator of snuggles! With its sleek and understated design, this plushie strikes the perfect balance between cuddle companion and imaginary ocean guardian. Beware; opponents may be mesmerized by its dorsal fin's hypnotic sway! loadout-description-LoadoutItemPlushieCarp = Brace for extraterrestrial antics with the Purple Space Carp Plushie! A fishy invader from the cosmic deep, this plushie brings a splash of humor to zero-gravity escapades. From hostile waters to interstellar giggles, it's a cuddly contradiction that's out of this world +loadout-description-LoadoutSolCommonTranslator = The most common of all translators, such that it can be purchased in any civilized station. + This device translates Sol Common speech into Galactic Common. diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index 1f90b421526..e6c08bf90b9 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -607,6 +607,20 @@ - type: Clothing sprite: Clothing/Belt/securitywebbing.rsi +- type: entity + parent: ClothingBeltSecurityWebbing + id: ClothingBeltSecurityWebbingFilled + name: security webbing + description: Unique and versatile chest rig, can hold security gear. + components: + - type: StorageFill + contents: + - id: GrenadeFlashBang + - id: TearGasGrenade + - id: Stunbaton + - id: Handcuffs + - id: Handcuffs + - type: entity parent: ClothingBeltStorageBase id: ClothingBeltMercWebbing diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml index 659cbaa28a2..18ea198697e 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/cigarette.yml @@ -80,7 +80,7 @@ - ReagentId: Nicotine Quantity: 10 - ReagentId: Omnizine - Quantity: 30 + Quantity: 5 - type: entity id: CigaretteOmnizine diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml index bc9079fb2db..eb8e0a1ffe6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml @@ -182,7 +182,7 @@ id: CigPackSyndicate parent: CigPackBase name: Interdyne herbals packet - description: Elite cigarettes for elite syndicate agents. Infused with medicine for when you need to do more than calm your nerves. + description: Premium medicinal cigarettes from the Interdyne Corporation. Not endorsed by the Terra-Gov Surgeon General. components: - type: Sprite sprite: Objects/Consumable/Smokeables/Cigarettes/Packs/syndicate.rsi diff --git a/Resources/Prototypes/Loadouts/Jobs/medical.yml b/Resources/Prototypes/Loadouts/Jobs/medical.yml index 5e88006fcee..6dcce11d09f 100644 --- a/Resources/Prototypes/Loadouts/Jobs/medical.yml +++ b/Resources/Prototypes/Loadouts/Jobs/medical.yml @@ -9,6 +9,8 @@ - MedicalDoctor - Paramedic - ChiefMedicalOfficer + - MedicalIntern + - Chemist items: - ClothingHandsGlovesNitrile @@ -35,6 +37,7 @@ jobs: - MedicalDoctor - ChiefMedicalOfficer + - MedicalIntern items: - ClothingNeckStethoscope @@ -47,6 +50,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern items: - UniformScrubsColorBlue @@ -59,6 +65,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern items: - UniformScrubsColorGreen @@ -71,9 +80,85 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern items: - UniformScrubsColorPurple +- type: loadout + id: LoadoutMedicalUniformScrubsCyan + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - UniformScrubsColorCyan + +- type: loadout + id: LoadoutMedicalUniformScrubsBlack + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - UniformScrubsColorBlack + +- type: loadout + id: LoadoutMedicalUniformScrubsPink + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - UniformScrubsColorPink + +- type: loadout + id: LoadoutMedicalUniformScrubsCybersun + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + items: + - UniformScrubsColorCybersun + +- type: loadout + id: LoadoutMedicalOuterCybersunWindbreaker + category: Jobs + cost: 5 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + items: + - ClothingOuterCoatCybersunWindbreaker + - type: loadout id: LoadoutMedicalOuterLabcoatChem category: Jobs @@ -130,6 +215,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - ChiefMedicalOfficer - !type:CharacterPlaytimeRequirement tracker: JobChemist min: 21600 # 6 hours @@ -151,6 +239,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - ChiefMedicalOfficer - !type:CharacterPlaytimeRequirement tracker: JobChemist min: 21600 # 6 hours @@ -184,6 +275,9 @@ - !type:CharacterJobRequirement jobs: - MedicalDoctor + - Chemist + - Paramedic + - ChiefMedicalOfficer - !type:CharacterPlaytimeRequirement tracker: JobChemist min: 21600 # 6 hours @@ -195,3 +289,173 @@ min: 216000 # 60 hours items: - ClothingHeadHatBeretSeniorPhysician + +- type: loadout + id: LoadoutMedicalHeadSurgcapBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapBlue + +- type: loadout + id: LoadoutMedicalHeadSurgcapPurple + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapPurple + +- type: loadout + id: LoadoutMedicalHeadSurgcapGreen + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapGreen + +- type: loadout + id: LoadoutMedicalHeadSurgcapCyan + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapCyan + +- type: loadout + id: LoadoutMedicalHeadSurgcapBlack + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapBlack + +- type: loadout + id: LoadoutMedicalHeadSurgcapPink + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapPink + +- type: loadout + id: LoadoutMedicalHeadSurgcapWhite + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + - MedicalIntern + items: + - ClothingHeadHatSurgcapWhite + +- type: loadout + id: LoadoutMedicalHeadSurgcapCybersun + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Chemist + - Paramedic + items: + - ClothingHeadHatSurgcapCybersun + +- type: loadout + id: LoadoutMedicalEyesHudMedical + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Paramedic + - ChiefMedicalOfficer + - MedicalIntern + - Brigmedic + items: + - ClothingEyesHudMedical + +- type: loadout + id: LoadoutMedicalEyesEyepatchHudMedical + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Paramedic + - ChiefMedicalOfficer + - MedicalIntern + - Brigmedic + items: + - ClothingEyesEyepatchHudMedical + +- type: loadout + id: LoadoutMedicalEyesHudMedicalPrescription + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - MedicalDoctor + - Paramedic + - ChiefMedicalOfficer + - MedicalIntern + - Brigmedic + - !type:CharacterTraitRequirement + traits: + - Nearsighted + items: + - ClothingEyesPrescriptionMedHud diff --git a/Resources/Prototypes/Loadouts/Jobs/science.yml b/Resources/Prototypes/Loadouts/Jobs/science.yml index ad6f02e589e..b9c815a15b0 100644 --- a/Resources/Prototypes/Loadouts/Jobs/science.yml +++ b/Resources/Prototypes/Loadouts/Jobs/science.yml @@ -84,3 +84,59 @@ - ResearchDirector items: - ClothingHeadHatBeretRND + +- type: loadout + id: LoadoutScienceEyesHudDiagnostic + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector + items: + - ClothingEyesHudDiagnostic + +- type: loadout + id: LoadoutScienceEyesEyepatchHudDiag + category: Jobs + cost: 3 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector + items: + - ClothingEyesEyepatchHudDiag + +## Robes +- type: loadout + id: LoadoutOuterRobeTechPriest + category: Outer + cost: 2 + items: + - ClothingOuterRobeTechPriest + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector + +- type: loadout + id: LoadoutHeadHoodTechPriest + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadTechPriest + requirements: + - !type:CharacterJobRequirement + jobs: + - Scientist + - ResearchAssistant + - ResearchDirector diff --git a/Resources/Prototypes/Loadouts/Jobs/security.yml b/Resources/Prototypes/Loadouts/Jobs/security.yml index ecf7e4893a2..e6a6693ec18 100644 --- a/Resources/Prototypes/Loadouts/Jobs/security.yml +++ b/Resources/Prototypes/Loadouts/Jobs/security.yml @@ -1,7 +1,22 @@ +## Uniforms - type: loadout - id: LoadoutSecurityUniformGrey + id: LoadoutSecurityUniformJumpsuitBlue category: Jobs - cost: 2 + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - SecurityCadet + - Warden + items: + - ClothingUniformJumpsuitSecBlue + +- type: loadout + id: LoadoutSecurityUniformJumpsuitGrey + category: Jobs + cost: 1 exclusive: true requirements: - !type:CharacterJobRequirement @@ -12,6 +27,34 @@ items: - ClothingUniformJumpsuitSecGrey +- type: loadout + id: LoadoutSecurityUniformJumpskirtGrey + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - SecurityCadet + - Warden + items: + - ClothingUniformJumpskirtSecGrey + +- type: loadout + id: LoadoutSecurityUniformJumpskirtBlue + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - SecurityCadet + - Warden + items: + - ClothingUniformJumpskirtSecBlue + - type: loadout id: LoadoutSecurityUniformJumpskirtSenior category: Jobs @@ -60,6 +103,108 @@ items: - ClothingUniformJumpsuitSeniorOfficer +- type: loadout + id: LoadoutUniformJumpsuitWardenBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpsuitWardenBlue + +- type: loadout + id: LoadoutUniformJumpsuitWardenGrey + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpsuitWardenGrey + +- type: loadout + id: LoadoutUniformJumpskirtWardenBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpskirtWardenBlue + +- type: loadout + id: LoadoutUniformJumpskirtWardenGrey + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Warden + items: + - ClothingUniformJumpskirtWardenGrey + +- type: loadout + id: LoadoutUniformJumpskirtHoSBlue + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - HeadOfSecurity + items: + - ClothingUniformJumpskirtHoSBlue + +- type: loadout + id: LoadoutUniformJumpskirtHoSGrey + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - HeadOfSecurity + items: + - ClothingUniformJumpskirtHoSGrey + +- type: loadout + id: LoadoutUniformJumpsuitSecFormal + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Detective + - SecurityOfficer + - Warden + - HeadOfSecurity + items: + - ClothingUniformJumpsuitSecFormal + +- type: loadout + id: LoadoutUniformJumpsuitSecSummer + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Detective + - SecurityOfficer + - Warden + - HeadOfSecurity + items: + - ClothingUniformJumpsuitSecSummer +## Mask - type: loadout id: LoadoutSecurityMaskGasSwat category: Jobs @@ -73,6 +218,7 @@ items: - ClothingMaskGasSwat +## Shoes - type: loadout id: LoadoutSecurityShoesJackboots category: Jobs @@ -87,3 +233,119 @@ - HeadOfSecurity items: - ClothingShoesBootsJack + +## Eyes +- type: loadout + id: LoadoutSecurityEyesHudSecurity + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingEyesHudSecurity + +- type: loadout + id: LoadoutSecurityEyesEyepatchHudSecurity + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingEyesEyepatchHudSecurity + +- type: loadout + id: LoadoutSecurityEyesHudSecurityPrescription + category: Jobs + cost: 2 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + - !type:CharacterTraitRequirement + traits: + - Nearsighted + items: + - ClothingEyesPrescriptionHudSecurity + +## Head +- type: loadout + id: LoadoutSecurityHeadHatBeret + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingHeadHatBeretSecurity + +- type: loadout + id: LoadoutSecurityHeadHelmetInsulated + category: Jobs + cost: 3 + requirements: + - !type:CharacterJobRequirement + jobs: + - SecurityOfficer + - Warden + - HeadOfSecurity + - Brigmedic + items: + - ClothingHeadHelmetInsulated + +## Belt +- type: loadout + id: LoadoutSecurityBeltWebbing + category: Jobs + cost: 1 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Detective + - SecurityOfficer + - SecurityCadet + - Warden + - HeadOfSecurity + items: + - ClothingBeltSecurityWebbingFilled + +## Species +#- type: loadout ##Uncomment this and reassess points when we can make it replace the secoff duty pistol +# id: LoadoutSecurityEquipmentTruncheon +# category: Jobs +# cost: 8 ## TODO: Make this replace the secoff handgun, and thus also make it cheaper +# requirements: +# - !type:CharacterJobRequirement +# jobs: +# - SecurityOfficer +# - Warden +# - HeadOfSecurity +# - Brigmedic +# - !type:CharacterPlaytimeRequirement +# tracker: JobSecurityOfficer +# min: 36000 # 10 hours +# - !type:CharacterSpeciesRequirement +# species: Oni +# items: +# - Truncheon diff --git a/Resources/Prototypes/Loadouts/Jobs/service.yml b/Resources/Prototypes/Loadouts/Jobs/service.yml index 6ef3c3ad485..ed6107db9e9 100644 --- a/Resources/Prototypes/Loadouts/Jobs/service.yml +++ b/Resources/Prototypes/Loadouts/Jobs/service.yml @@ -1,3 +1,4 @@ +## Clown - type: loadout id: LoadoutServiceClownOutfitJester category: Jobs @@ -26,6 +27,7 @@ - ClothingHeadHatJesterAlt - ClothingShoesJester +## Bartender - type: loadout id: LoadoutServiceBartenderUniformPurple category: Jobs @@ -38,6 +40,7 @@ items: - ClothingUniformJumpsuitBartenderPurple +## Botanist - type: loadout id: LoadoutServiceBotanistUniformOveralls category: Jobs @@ -50,6 +53,7 @@ items: - ClothingUniformOveralls +## Lawyer - type: loadout id: LoadoutServiceLawyerUniformBlueSuit category: Jobs @@ -158,6 +162,7 @@ items: - ClothingUniformJumpsuitJournalist +## Reporter - type: loadout id: LoadoutServiceReporterUniformDetectivesuit category: Jobs @@ -181,3 +186,15 @@ - Reporter items: - ClothingUniformJumpskirtDetective + +## Musician +- type: loadout + id: LoadoutItemSynthesizerInstrument + category: Jobs + cost: 8 + requirements: + - !type:CharacterJobRequirement + jobs: + - Musician + items: + - SynthesizerInstrument diff --git a/Resources/Prototypes/Loadouts/categories.yml b/Resources/Prototypes/Loadouts/categories.yml index a4381941acc..79d2d7fe2bf 100644 --- a/Resources/Prototypes/Loadouts/categories.yml +++ b/Resources/Prototypes/Loadouts/categories.yml @@ -4,7 +4,13 @@ id: Uncategorized - type: loadoutCategory - id: Accessories + id: Eyes + +- type: loadoutCategory + id: Hands + +- type: loadoutCategory + id: Head - type: loadoutCategory id: Items @@ -12,8 +18,17 @@ - type: loadoutCategory id: Jobs +- type: loadoutCategory + id: Neck + - type: loadoutCategory id: Outer +- type: loadoutCategory + id: Shoes + +- type: loadoutCategory + id: Species + - type: loadoutCategory id: Uniform diff --git a/Resources/Prototypes/Loadouts/eyes.yml b/Resources/Prototypes/Loadouts/eyes.yml index a7a8cbd7736..74226604e92 100644 --- a/Resources/Prototypes/Loadouts/eyes.yml +++ b/Resources/Prototypes/Loadouts/eyes.yml @@ -1,13 +1,21 @@ - type: loadout id: LoadoutEyesEyepatch - category: Accessories + category: Eyes cost: 1 items: - ClothingEyesEyepatch - type: loadout id: LoadoutEyesBlindfold - category: Accessories + category: Eyes cost: 2 items: - ClothingEyesBlindfold + +- type: loadout + id: LoadoutItemSunglasses + category: Eyes + cost: 5 + exclusive: true + items: + - ClothingEyesGlassesSunglasses diff --git a/Resources/Prototypes/Loadouts/hands.yml b/Resources/Prototypes/Loadouts/hands.yml new file mode 100644 index 00000000000..3604678d387 --- /dev/null +++ b/Resources/Prototypes/Loadouts/hands.yml @@ -0,0 +1,115 @@ +- type: loadout + id: LoadoutHandsColorPurple + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorPurple + +- type: loadout + id: LoadoutHandsColorRed + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorRed + +- type: loadout + id: LoadoutHandsColorBlack + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorBlack + +- type: loadout + id: LoadoutHandsColorBlue + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorBlue + +- type: loadout + id: LoadoutHandsColorBrown + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorBrown + +- type: loadout + id: LoadoutHandsColorGray + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorGray + +- type: loadout + id: LoadoutHandsColorGreen + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorGreen + +- type: loadout + id: LoadoutHandsColorLightBrown + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorLightBrown + +- type: loadout + id: LoadoutHandsColorOrange + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorOrange + +- type: loadout + id: LoadoutHandsColorWhite + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesColorWhite + +- type: loadout + id: LoadoutHandsColorYellowBudget + category: Hands + cost: 4 + exclusive: true + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger + items: + - ClothingHandsGlovesColorYellowBudget + +- type: loadout + id: LoadoutHandsGlovesLeather + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesLeather + +- type: loadout + id: LoadoutHandsGlovesPowerglove + category: Hands + cost: 2 + exclusive: true + items: + - ClothingHandsGlovesPowerglove + +- type: loadout + id: LoadoutHandsGlovesRobohands + category: Hands + cost: 1 + exclusive: true + items: + - ClothingHandsGlovesRobohands diff --git a/Resources/Prototypes/Loadouts/head.yml b/Resources/Prototypes/Loadouts/head.yml index 33a2f0b19bc..25cb4dadf11 100644 --- a/Resources/Prototypes/Loadouts/head.yml +++ b/Resources/Prototypes/Loadouts/head.yml @@ -1,125 +1,273 @@ +## Hats - type: loadout id: LoadoutHeadBeaverHat - category: Accessories + category: Head cost: 2 + exclusive: true items: - ClothingHeadHatBeaverHat - type: loadout id: LoadoutHeadTophat - category: Accessories + category: Head cost: 2 + exclusive: true items: - ClothingHeadHatTophat +- type: loadout + id: LoadoutHeadFedoraBlack + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFedoraBlack + +- type: loadout + id: LoadoutHeadFedoraChoc + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFedoraChoc + +- type: loadout + id: LoadoutHeadFedoraWhite + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFedoraWhite + +- type: loadout + id: LoadoutHeadFlatBlack + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFlatBlack + +- type: loadout + id: LoadoutHeadFlatBrown + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatFlatBrown + +- type: loadout + id: LoadoutHeadTinfoil + category: Head + cost: 3 + exclusive: true + items: + - ClothingHeadTinfoil + +- type: loadout + id: LoadoutHeadBellhop + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadHatBellhop +## Color Hats - type: loadout id: LoadoutHeadHatBluesoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatBluesoft - type: loadout id: LoadoutHeadHatBluesoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatBluesoftFlipped - type: loadout id: LoadoutHeadHatCorpsoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatCorpsoft - type: loadout id: LoadoutHeadHatCorpsoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatCorpsoftFlipped - type: loadout id: LoadoutHeadHatGreensoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreensoft - type: loadout id: LoadoutHeadHatGreensoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreensoftFlipped - type: loadout id: LoadoutHeadHatGreysoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreysoft - type: loadout id: LoadoutHeadHatGreysoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatGreysoftFlipped - type: loadout id: LoadoutHeadHatOrangesoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatOrangesoft - type: loadout id: LoadoutHeadHatOrangesoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatOrangesoftFlipped - type: loadout id: LoadoutHeadHatPurplesoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatPurplesoft - type: loadout id: LoadoutHeadHatPurplesoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatPurplesoftFlipped - type: loadout id: LoadoutHeadHatRedsoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatRedsoft - type: loadout id: LoadoutHeadHatRedsoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatRedsoftFlipped - type: loadout id: LoadoutHeadHatYellowsoft - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatYellowsoft - type: loadout id: LoadoutHeadHatYellowsoftFlipped - category: Accessories + category: Head cost: 1 + exclusive: true items: - ClothingHeadHatYellowsoftFlipped + +## Headbands +- type: loadout + id: LoadoutHeadBandBlack + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandBlack + +- type: loadout + id: LoadoutHeadBandBlue + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandBlue + +- type: loadout + id: LoadoutHeadBandGold + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandGold + +- type: loadout + id: LoadoutHeadBandGreen + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandGreen + +- type: loadout + id: LoadoutHeadBandGrey + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandGrey + +- type: loadout + id: LoadoutHeadBandRed + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandRed + +- type: loadout + id: LoadoutHeadBandSkull + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandSkull + +- type: loadout + id: LoadoutHeadBandMerc + category: Head + cost: 2 + exclusive: true + items: + - ClothingHeadBandMerc + +- type: loadout + id: LoadoutHeadBandBrown + category: Head + cost: 1 + exclusive: true + items: + - ClothingHeadBandBrown diff --git a/Resources/Prototypes/Loadouts/items.yml b/Resources/Prototypes/Loadouts/items.yml index 6ce8d260613..072061d2e28 100644 --- a/Resources/Prototypes/Loadouts/items.yml +++ b/Resources/Prototypes/Loadouts/items.yml @@ -1,3 +1,4 @@ +#Smokes - type: loadout id: LoadoutItemCig category: Items @@ -34,11 +35,18 @@ - CigPackBlack - type: loadout - id: LoadoutItemPAI + id: LoadoutItemCigsMixed category: Items cost: 3 items: - - PersonalAI + - CigPackMixed + +- type: loadout + id: LoadoutItemCigsSyndicate + category: Items + cost: 4 + items: + - CigPackSyndicate - type: loadout id: LoadoutItemLighter @@ -61,30 +69,138 @@ items: - Matchbox +## Instruments - type: loadout - id: LoadoutItemPlushieSharkBlue + id: LoadoutItemMicrophoneInstrument category: Items - cost: 2 + cost: 4 items: - - PlushieSharkBlue + - MicrophoneInstrument - type: loadout - id: LoadoutItemPlushieSharkPink + id: LoadoutItemKalimbaInstrument category: Items - cost: 2 + cost: 4 + items: + - KalimbaInstrument + +- type: loadout + id: LoadoutItemTrumpetInstrument + category: Items + cost: 6 + items: + - TrumpetInstrument + +- type: loadout + id: LoadoutItemElectricGuitar + category: Items + cost: 7 + items: + - ElectricGuitarInstrument + +- type: loadout + id: LoadoutItemBassGuitar + category: Items + cost: 7 + items: + - BassGuitarInstrument + +- type: loadout + id: LoadoutItemRockGuitar + category: Items + cost: 7 + items: + - RockGuitarInstrument + +- type: loadout + id: LoadoutItemAcousticGuitar + category: Items + cost: 7 + items: + - AcousticGuitarInstrument + +- type: loadout + id: LoadoutItemViolin + category: Items + cost: 6 + items: + - ViolinInstrument + +- type: loadout + id: LoadoutItemHarmonica + category: Items + cost: 3 + items: + - HarmonicaInstrument + +- type: loadout + id: LoadoutItemAccordion + category: Items + cost: 6 + items: + - AccordionInstrument + +- type: loadout + id: LoadoutItemFlute + category: Items + cost: 4 items: - - PlushieSharkPink + - FluteInstrument - type: loadout - id: LoadoutItemPlushieSharkGrey + id: LoadoutItemOcarina + category: Items + cost: 3 + items: + - OcarinaInstrument + +# Survival Kit +- type: loadout + id: LoadoutItemsEmergencyOxygenTank + category: Items + cost: 1 + items: + - EmergencyOxygenTankFilled + +- type: loadout + id: LoadoutItemsExtendedEmergencyOxygenTank category: Items cost: 2 items: - - PlushieSharkGrey + - ExtendedEmergencyOxygenTankFilled + +- type: loadout + id: LoadoutItemsDoubleEmergencyOxygenTank + category: Items + cost: 4 + items: + - DoubleEmergencyOxygenTankFilled + +- type: loadout + id: LoadoutItemsEmergencyCrowbar + category: Items + cost: 3 + items: + - CrowbarRed +#Misc Items - type: loadout - id: LoadoutItemPlushieCarp + id: LoadoutItemPAI + category: Items + cost: 3 + items: + - PersonalAI + +- type: loadout + id: LoadoutItemWaistbag category: Items cost: 2 items: - - PlushieCarp + - ClothingBeltStorageWaistbag + +- type: loadout + id: LoadoutSolCommonTranslator + category: Items + cost: 3 + items: + - SolCommonTranslator diff --git a/Resources/Prototypes/Loadouts/neck.yml b/Resources/Prototypes/Loadouts/neck.yml index 7e5526f966a..eb933de29ee 100644 --- a/Resources/Prototypes/Loadouts/neck.yml +++ b/Resources/Prototypes/Loadouts/neck.yml @@ -1,27 +1,104 @@ - type: loadout id: LoadoutNeckScarfStripedRed - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedRed - type: loadout id: LoadoutNeckScarfStripedBlue - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedBlue - type: loadout id: LoadoutNeckScarfStripedGreen - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedGreen - type: loadout id: LoadoutNeckScarfStripedZebra - category: Accessories + category: Neck cost: 1 + exclusive: true items: - ClothingNeckScarfStripedZebra + +#Pride Accessories +- type: loadout + id: LoadoutItemsPrideLGBTPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckLGBTPin + +- type: loadout + id: LoadoutItemsPrideAromanticPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckAromanticPin + +- type: loadout + id: LoadoutItemsPrideAsexualPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckAsexualPin + +- type: loadout + id: LoadoutItemsPrideBisexualPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckBisexualPin + +- type: loadout + id: LoadoutItemsPrideIntersexPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckIntersexPin + +- type: loadout + id: LoadoutItemsPrideLesbianPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckLesbianPin + +- type: loadout + id: LoadoutItemsPrideNonBinaryPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckNonBinaryPin + +- type: loadout + id: LoadoutItemsPridePansexualPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckPansexualPin + +- type: loadout + id: LoadoutItemsPrideTransPin + category: Neck + cost: 1 + exclusive: true + items: + - ClothingNeckTransPin diff --git a/Resources/Prototypes/Loadouts/outerClothing.yml b/Resources/Prototypes/Loadouts/outerClothing.yml index 078cf530ba2..a5932214ce9 100644 --- a/Resources/Prototypes/Loadouts/outerClothing.yml +++ b/Resources/Prototypes/Loadouts/outerClothing.yml @@ -1,10 +1,3 @@ -- type: loadout - id: LoadoutOuterGhostSheet - category: Outer - cost: 2 - items: - - ClothingOuterGhostSheet - - type: loadout id: LoadoutOuterCoatBomberjacket category: Outer @@ -33,6 +26,72 @@ items: - ClothingOuterWinterCoat +- type: loadout + id: LoadoutOuterCoatHyenhSweater + category: Outer + cost: 3 + items: + - ClothingOuterCoatHyenhSweater + +- type: loadout + id: LoadoutOuterWinterCoatLong + category: Outer + cost: 3 + items: + - ClothingOuterWinterCoatLong + +- type: loadout + id: LoadoutOuterWinterCoatPlaid + category: Outer + cost: 3 + items: + - ClothingOuterWinterCoatPlaid + +- type: loadout + id: LoadoutOuterVestValet + category: Outer + cost: 1 + items: + - ClothingOuterVestValet + +## Letterman Jackets +- type: loadout + id: LoadoutOuterCoatLettermanBlue + category: Outer + cost: 3 + items: + - ClothingOuterCoatLettermanBlue + +- type: loadout + id: LoadoutOuterCoatLettermanRed + category: Outer + cost: 3 + items: + - ClothingOuterCoatLettermanRed + +## MNK +- type: loadout + id: LoadoutOuterCoatMNKWhiteHoodie + category: Outer + cost: 3 + items: + - ClothingOuterCoatMNKWhiteHoodie + +- type: loadout + id: LoadoutOuterCoatMNKBlackTopCoat + category: Outer + cost: 3 + items: + - ClothingOuterCoatMNKBlackTopCoat + +- type: loadout + id: LoadoutOuterCoatMNKBlackJacket + category: Outer + cost: 3 + items: + - ClothingOuterCoatMNKBlackJacket + +## Contractor Jackets - type: loadout id: LoadoutOuterCorporateJacket category: Outer diff --git a/Resources/Prototypes/Loadouts/shoes.yml b/Resources/Prototypes/Loadouts/shoes.yml index 4a1880b5e21..3c2e21e631b 100644 --- a/Resources/Prototypes/Loadouts/shoes.yml +++ b/Resources/Prototypes/Loadouts/shoes.yml @@ -1,7 +1,7 @@ # Colored - type: loadout id: LoadoutShoesBlack - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -9,7 +9,7 @@ - type: loadout id: LoadoutShoesBlue - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -17,7 +17,7 @@ - type: loadout id: LoadoutShoesBrown - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -25,7 +25,7 @@ - type: loadout id: LoadoutShoesGreen - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -33,7 +33,7 @@ - type: loadout id: LoadoutShoesOrange - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -41,7 +41,7 @@ - type: loadout id: LoadoutShoesPurple - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -49,7 +49,7 @@ - type: loadout id: LoadoutShoesRed - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -57,7 +57,7 @@ - type: loadout id: LoadoutShoesWhite - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -65,25 +65,93 @@ - type: loadout id: LoadoutShoesYellow - category: Accessories + category: Shoes cost: 1 exclusive: true items: - ClothingShoesColorYellow +- type: loadout + id: LoadoutShoesGeta + category: Shoes + cost: 1 + exclusive: true + items: + - ClothingShoesGeta + +## Boots +- type: loadout + id: LoadoutShoesBootsWork + category: Shoes + cost: 1 + exclusive: true + items: + - ClothingShoesBootsWork + +- type: loadout + id: LoadoutShoesBootsLaceup + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsLaceup + +- type: loadout + id: LoadoutShoesBootsWinter + category: Shoes + cost: 1 + exclusive: true + items: + - ClothingShoesBootsWinter + +- type: loadout + id: LoadoutShoesBootsCowboyBrown + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyBrown + +- type: loadout + id: LoadoutShoesBootsCowboyBlack + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyBlack + +- type: loadout + id: LoadoutShoesBootsCowboyWhite + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyWhite + +- type: loadout + id: LoadoutShoesBootsCowboyFancy + category: Shoes + cost: 2 + exclusive: true + items: + - ClothingShoesBootsCowboyFancy # Miscellaneous - type: loadout id: LoadoutShoesSlippersDuck - category: Accessories - cost: 1 + category: Shoes + cost: 2 exclusive: true items: - ClothingShoeSlippersDuck + requirements: + - !type:CharacterJobRequirement + jobs: + - Clown - type: loadout id: LoadoutShoesLeather - category: Accessories + category: Shoes cost: 1 exclusive: true items: @@ -91,7 +159,7 @@ - type: loadout id: LoadoutShoesMiscWhite - category: Accessories + category: Shoes cost: 1 exclusive: true items: diff --git a/Resources/Prototypes/Loadouts/species.yml b/Resources/Prototypes/Loadouts/species.yml new file mode 100644 index 00000000000..1d2fb58dc0a --- /dev/null +++ b/Resources/Prototypes/Loadouts/species.yml @@ -0,0 +1,35 @@ +- type: loadout + id: LoadoutSpeciesEmergencyNitrogenTank + category: Species + cost: 0 + requirements: + - !type:CharacterSpeciesRequirement + species: SlimePerson + - !type:CharacterSpeciesRequirement + species: Vox + items: + - EmergencyNitrogenTankFilled + +- type: loadout + id: LoadoutSpeciesExtendedEmergencyNitrogenTank + category: Species + cost: 1 + requirements: + - !type:CharacterSpeciesRequirement + species: SlimePerson + - !type:CharacterSpeciesRequirement + species: Vox + items: + - ExtendedEmergencyNitrogenTankFilled + +- type: loadout + id: LoadoutSpeciesDoubleEmergencyNitrogenTank + category: Species + cost: 3 + requirements: + - !type:CharacterSpeciesRequirement + species: SlimePerson + - !type:CharacterSpeciesRequirement + species: Vox + items: + - DoubleEmergencyNitrogenTankFilled diff --git a/Resources/Prototypes/Loadouts/uniform.yml b/Resources/Prototypes/Loadouts/uniform.yml index 5afdd303c0b..20c5b42c7c2 100644 --- a/Resources/Prototypes/Loadouts/uniform.yml +++ b/Resources/Prototypes/Loadouts/uniform.yml @@ -19,6 +19,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorBlack + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorBlack @@ -27,6 +31,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorBlack + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorBlue @@ -35,6 +43,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorBlue @@ -43,6 +55,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorGreen @@ -51,6 +67,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorGreen @@ -59,6 +79,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorOrange @@ -67,6 +91,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorOrange + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorOrange @@ -75,6 +103,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorOrange + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorPink @@ -83,6 +115,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorPink + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorPink @@ -91,6 +127,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorPink + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorRed @@ -99,6 +139,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorRed + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorRed @@ -107,6 +151,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorRed + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorWhite @@ -115,6 +163,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorWhite + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorWhite @@ -123,6 +175,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorWhite + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorYellow @@ -131,6 +187,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorYellow + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorYellow @@ -139,6 +199,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorYellow + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorDarkBlue @@ -147,6 +211,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorDarkBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorDarkBlue @@ -155,6 +223,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorDarkBlue + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorTeal @@ -163,6 +235,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorTeal + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorTeal @@ -171,6 +247,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorTeal + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorPurple @@ -179,6 +259,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorPurple + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorPurple @@ -187,6 +271,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorPurple + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorDarkGreen @@ -195,6 +283,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorDarkGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorDarkGreen @@ -203,6 +295,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorDarkGreen + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorLightBrown @@ -211,6 +307,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorLightBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorLightBrown @@ -219,6 +319,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorLightBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorBrown @@ -227,6 +331,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorBrown @@ -235,6 +343,10 @@ exclusive: true items: - ClothingUniformJumpskirtColorBrown + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpsuitColorMaroon @@ -243,6 +355,10 @@ exclusive: true items: - ClothingUniformJumpsuitColorMaroon + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger - type: loadout id: LoadoutUniformJumpskirtColorMaroon @@ -251,11 +367,131 @@ exclusive: true items: - ClothingUniformJumpskirtColorMaroon + requirements: + - !type:CharacterJobRequirement + jobs: + - Passenger + +## Kendo +- type: loadout + id: LoadoutUniformKendoHakama + category: Uniform + cost: 2 + exclusive: true + items: + - ClothingUniformKendoHakama + +- type: loadout + id: LoadoutUniformMartialGi + category: Uniform + cost: 2 + exclusive: true + items: + - ClothingUniformMartialGi + +## Kimono +- type: loadout + id: LoadoutClothingKimonoBlue + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoBlue + +- type: loadout + id: LoadoutClothingKimonoPink + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoPink + +- type: loadout + id: LoadoutClothingKimonoPurple + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoPurple + +- type: loadout + id: LoadoutClothingKimonoSky + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoSky + +- type: loadout + id: LoadoutClothingKimonoGreen + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingKimonoGreen +## Gakuran - type: loadout - id: LoadoutUniformJumpsuitColorRainbow + id: LoadoutUniformSchoolGakuranBlack category: Uniform cost: 2 exclusive: true items: - - ClothingUniformColorRainbow + - ClothingUniformSchoolGakuranBlack + +## MNK Uniforms +- type: loadout + id: LoadoutClothingMNKOfficeSkirt + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKOfficeSkirt + +- type: loadout + id: LoadoutClothingMNKUnderGarment + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKUnderGarment + +- type: loadout + id: LoadoutClothingMNKGymBra + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKGymBra + +- type: loadout + id: LoadoutClothingMNKDressBlack + category: Uniform + cost: 4 + exclusive: true + items: + - ClothingUniformMNKDressBlack + +- type: loadout + id: LoadoutClothingMNKBlackOveralls + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKBlackOveralls + +- type: loadout + id: LoadoutClothingMNKBlackShoulder + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKBlackShoulder + +- type: loadout + id: LoadoutClothingMNKTracksuitBlack + category: Uniform + cost: 3 + exclusive: true + items: + - ClothingUniformMNKTracksuitBlack From 529c5a4a9fd1fd613f9f1931723300183cd37318 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 22:53:58 +0000 Subject: [PATCH 16/21] Automatic Changelog Update (#425) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index cc18a7b1e00..68ccf158b46 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4343,3 +4343,11 @@ Entries: configure the frequency on their server manually. id: 6143 time: '2024-07-05T21:03:28.0000000+00:00' +- author: VMSolidus + changes: + - type: Add + message: >- + The catalog of items available via Loadouts has been greatly expanded. + Have fun customizing your characters! + id: 6144 + time: '2024-07-05T22:53:36.0000000+00:00' From 5d3648305a621c393375521469a2ae86688cff2f Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 5 Jul 2024 19:49:31 -0400 Subject: [PATCH 17/21] Trichromat Modification Trait (#505) # Description This PR brings back a previous briefly existing feature, that of the NormalVisionComponent, which now exists as a single point positive trait that can be taken only by Harpies and Vulpkanin(A list to later be expanded if we ever get new species that have modified vision types). This trait is called "Trichromat Modification", and it simply removes any species based vision modifications an entity may have, such as Ultraviolet Vision. :cl: - add: Trichromat Modification has been added as a new positive trait. It can be taken by Harpies and Vulpkanin to buy off their unique vision negative traits. Signed-off-by: VMSolidus --- .../Components/NormalVisionComponent.cs | 12 ++++++++++ .../Assorted/Systems/NormalVisionSystem.cs | 23 +++++++++++++++++++ Resources/Locale/en-US/traits/traits.ftl | 6 +++++ .../Prototypes/DeltaV/Traits/altvision.yml | 22 ++++++++++++++++++ Resources/Prototypes/Traits/neutral.yml | 20 ++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs create mode 100644 Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs diff --git a/Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs b/Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs new file mode 100644 index 00000000000..442bb6f0084 --- /dev/null +++ b/Content.Shared/Traits/Assorted/Components/NormalVisionComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Traits.Assorted.Components; + +/// +/// This is used for removing species specific vision traits +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class NormalVisionComponent : Component +{ +} + diff --git a/Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs b/Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs new file mode 100644 index 00000000000..ee25bf364b9 --- /dev/null +++ b/Content.Shared/Traits/Assorted/Systems/NormalVisionSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Abilities; +using Content.Shared.Traits.Assorted.Components; + +namespace Content.Shared.Traits.Assorted.Systems; + +/// +/// This handles removing species-specific vision traits. +/// +public sealed class NormalVisionSystem : EntitySystem +{ + /// + public override void Initialize() + { + SubscribeLocalEvent(OnStartup); + } + + + private void OnStartup(EntityUid uid, NormalVisionComponent component, ComponentInit args) + { + RemComp(uid); + RemComp(uid); + } +} diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 80680ac0db2..2f59d322282 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -32,6 +32,12 @@ trait-description-SocialAnxiety = You are anxious when you speak and stutter. trait-name-Snoring = Snoring trait-description-Snoring = You will snore while sleeping. +trait-name-NormalVisionHarpy = Trichromat Modification +trait-description-NormalVisionHarpy = Your eyes have been modified by means of advanced medicine to see in the standard colors of Red, Green, and Blue. + +trait-name-NormalVisionVulpkanin = Trichromat Modification +trait-description-NormalVisionVulpkanin = Your eyes have been modified by means of advanced medicine to see in the standard colors of Red, Green, and Blue. + trait-name-Thieving = Thieving trait-description-Thieving = You are deft with your hands, and talented at convincing people of their belongings. diff --git a/Resources/Prototypes/DeltaV/Traits/altvision.yml b/Resources/Prototypes/DeltaV/Traits/altvision.yml index d1980bc23ad..1257c1eeb09 100644 --- a/Resources/Prototypes/DeltaV/Traits/altvision.yml +++ b/Resources/Prototypes/DeltaV/Traits/altvision.yml @@ -2,6 +2,17 @@ id: UltraVision category: Visual points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Vulpkanin + - !type:CharacterSpeciesRequirement + inverted: true + species: Harpy + - !type:CharacterTraitRequirement + inverted: true + traits: + - DogVision components: - type: UltraVision @@ -9,5 +20,16 @@ id: DogVision category: Visual points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: Vulpkanin + - !type:CharacterSpeciesRequirement + inverted: true + species: Harpy + - !type:CharacterTraitRequirement + inverted: true + traits: + - UltraVision components: - type: DogVision diff --git a/Resources/Prototypes/Traits/neutral.yml b/Resources/Prototypes/Traits/neutral.yml index 3a3dc943cd7..86e12d20b34 100644 --- a/Resources/Prototypes/Traits/neutral.yml +++ b/Resources/Prototypes/Traits/neutral.yml @@ -21,3 +21,23 @@ - type: MothAccent - type: ReplacementAccent accent: dwarf + +- type: trait + id: NormalVisionHarpy + category: Visual + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + species: Harpy + components: + - type: NormalVision + +- type: trait + id: NormalVisionVulpkanin + category: Visual + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + species: Vulpkanin + components: + - type: NormalVision From 9d5f3b65dfbf93b4c4903e8a3ab3361182bed3f9 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Sat, 6 Jul 2024 02:49:47 +0300 Subject: [PATCH 18/21] Refactor the Language System (#459) # Description The language PR was merged early and OH GOD I ALREADY REGRET IT This PR is intended to provide the missing refactors and address the issues that were missed due to the early merge. --- # TODO - [X] Introduced a polymorphic obfuscation property to the LanguagePrototype - now it supports more than just 2 hardcoded methods, and each method can be configured per-language. Currently there are 3 obfuscation methods: replacement (same as replacement accent), obuscation by syllables and obfuscation by phrases. - [X] Refactored the existing obfuscation methods to not be a big hardcoded mess. - [X] Updated the existing languages accordingly: animalistic languages are now less of an unreadable mess and include less syllables. Certain languages like binary and snake seriously benefit from that. - [X] Refactored the existing commands in response to the never-addressed review (it got lost among hundreds of others) - [X] Refactored the commands to be more user-friendly (you can now use the number of the language in saylang and languageselect which can allow using keybinds to switch between languages) - [X] Moved a lot of obfuscation-related stuff from server to shared. The actual obfuscation process, however, is still done on the server. That may or may not be subject to change, too. - [X] Refactored the entire process of resolution of entities' languages. Instead of raising an event every time it's required to learn what languages an entity knows, the lists of ALL languages available to the entity (including via translators) is stored in LanguageSpeakerComponent and only updated when necessary (e.g. when a translator gets toggled). The list of languages the entity knows on its own is now stored in LanguageKnowledgeComponent. - [X] Made handheld translators automatically change your current language when activated. - [X] Rewrote the translator implanter system, now using the real implants and implanters - [ ] Rebalance science stuff (translators are incredibly expensive for what they're worth) - [ ] Uhhh stuff ---

Media

N/A for now

--- # Changelog :cl: - tweak: Translator implants are now proper implants that can be removed. - tweak: Animalistic languages should now look less messy. - fix: Hopefully fixed language menu desync and other issues. --------- Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> --- .../Language/LanguageMenuWindow.xaml.cs | 13 +- .../Language/Systems/LanguageSystem.cs | 1 - .../Systems/TranslatorImplanterSystem.cs | 8 - Content.Server/Chat/Systems/ChatSystem.cs | 6 +- .../Chemistry/ReagentEffects/MakeSentient.cs | 23 +- .../Language/Commands/ListLanguagesCommand.cs | 27 +- .../Language/Commands/SayLanguageCommand.cs | 6 +- .../Commands/SelectLanguageCommand.cs | 50 +- .../Language/DetermineEntityLanguagesEvent.cs | 32 +- .../Language/LanguageSystem.Networking.cs | 39 +- Content.Server/Language/LanguageSystem.cs | 325 +++---- .../Language}/LanguagesUpdateEvent.cs | 2 +- .../Language/TranslatorImplantSystem.cs | 66 ++ .../Language/TranslatorImplanterSystem.cs | 72 -- Content.Server/Language/TranslatorSystem.cs | 137 ++- .../Mind/Commands/MakeSentientCommand.cs | 7 +- .../Radio/EntitySystems/HeadsetSystem.cs | 2 +- .../Radio/EntitySystems/RadioSystem.cs | 4 +- .../Components/LanguageKnowledgeComponent.cs | 22 + .../Components/LanguageSpeakerComponent.cs | 33 +- .../Components/TranslatorImplantComponent.cs | 21 + .../TranslatorImplanterComponent.cs | 35 - .../Translators/BaseTranslatorComponent.cs | 9 - .../HandheldTranslatorComponent.cs | 15 +- Content.Shared/Language/LanguagePrototype.cs | 15 +- Content.Shared/Language/ObfuscationMethods.cs | 184 ++++ .../Language/Systems/SharedLanguageSystem.cs | 34 +- .../SharedTranslatorImplanterSystem.cs | 36 - Resources/Locale/en-US/language/commands.ftl | 16 +- .../DeltaV/Entities/Mobs/NPCs/animals.yml | 4 +- .../DeltaV/Entities/Mobs/NPCs/familiars.yml | 2 +- .../DeltaV/Entities/Mobs/NPCs/nukiemouse.yml | 2 +- .../DeltaV/Entities/Mobs/Species/harpy.yml | 2 +- .../Entities/Mobs/Species/vulpkanin.yml | 2 +- .../Mobs/Cyborgs/base_borg_chassis.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 38 +- .../Entities/Mobs/NPCs/argocyte.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/pets.yml | 20 +- .../Entities/Mobs/NPCs/regalrat.yml | 4 +- .../Prototypes/Entities/Mobs/NPCs/shadows.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/space.yml | 6 +- .../Prototypes/Entities/Mobs/NPCs/xeno.yml | 4 +- .../Prototypes/Entities/Mobs/Species/base.yml | 2 +- .../Entities/Mobs/Species/diona.yml | 2 +- .../Entities/Mobs/Species/dwarf.yml | 2 +- .../Entities/Mobs/Species/human.yml | 2 +- .../Prototypes/Entities/Mobs/Species/moth.yml | 2 +- .../Entities/Mobs/Species/reptilian.yml | 2 +- .../Entities/Mobs/Species/slime.yml | 2 +- Resources/Prototypes/Entities/Mobs/base.yml | 1 + .../Objects/Devices/translator_implants.yml | 168 ++-- .../Entities/Objects/Devices/translators.yml | 21 +- .../Objects/Misc/translator_implanters.yml | 77 ++ .../Structures/Machines/vending_machines.yml | 2 +- Resources/Prototypes/Language/languages.yml | 891 ++++++++++-------- .../Nyanotrasen/Entities/Mobs/Species/Oni.yml | 2 +- .../Entities/Mobs/Species/felinid.yml | 2 +- 59 files changed, 1398 insertions(+), 1114 deletions(-) delete mode 100644 Content.Client/Language/Systems/TranslatorImplanterSystem.cs rename {Content.Shared/Language/Events => Content.Server/Language}/LanguagesUpdateEvent.cs (78%) create mode 100644 Content.Server/Language/TranslatorImplantSystem.cs delete mode 100644 Content.Server/Language/TranslatorImplanterSystem.cs create mode 100644 Content.Shared/Language/Components/LanguageKnowledgeComponent.cs create mode 100644 Content.Shared/Language/Components/TranslatorImplantComponent.cs delete mode 100644 Content.Shared/Language/Components/TranslatorImplanterComponent.cs create mode 100644 Content.Shared/Language/ObfuscationMethods.cs delete mode 100644 Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs create mode 100644 Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml diff --git a/Content.Client/Language/LanguageMenuWindow.xaml.cs b/Content.Client/Language/LanguageMenuWindow.xaml.cs index 312814aca35..11d1c290d16 100644 --- a/Content.Client/Language/LanguageMenuWindow.xaml.cs +++ b/Content.Client/Language/LanguageMenuWindow.xaml.cs @@ -1,14 +1,8 @@ using Content.Client.Language.Systems; -using Content.Shared.Language; -using Content.Shared.Language.Systems; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Console; -using Robust.Shared.Utility; -using Serilog; -using static Content.Shared.Language.Systems.SharedLanguageSystem; namespace Content.Client.Language; @@ -121,8 +115,11 @@ private void AddLanguageEntry(string language) private void OnLanguageChosen(string id) { var proto = _clientLanguageSystem.GetLanguagePrototype(id); - if (proto != null) - _clientLanguageSystem.RequestSetLanguage(proto); + if (proto == null) + return; + + _clientLanguageSystem.RequestSetLanguage(proto); + UpdateState(id, _clientLanguageSystem.SpokenLanguages); } diff --git a/Content.Client/Language/Systems/LanguageSystem.cs b/Content.Client/Language/Systems/LanguageSystem.cs index 9714078b2c5..5dc2fc1f4e7 100644 --- a/Content.Client/Language/Systems/LanguageSystem.cs +++ b/Content.Client/Language/Systems/LanguageSystem.cs @@ -2,7 +2,6 @@ using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Robust.Client; -using Robust.Shared.Console; namespace Content.Client.Language.Systems; diff --git a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs b/Content.Client/Language/Systems/TranslatorImplanterSystem.cs deleted file mode 100644 index da19b3decf9..00000000000 --- a/Content.Client/Language/Systems/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.Language.Systems; - -namespace Content.Client.Language.Systems; - -public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem -{ - -} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 7eecaa32b43..9b6a7f540be 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -502,7 +502,7 @@ private void SendEntityWhisper( if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full) continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them. - var canUnderstandLanguage = _language.CanUnderstand(listener, language); + var canUnderstandLanguage = _language.CanUnderstand(listener, language.ID); // How the entity perceives the message depends on whether it can understand its language var perceivedMessage = canUnderstandLanguage ? message : languageObfuscatedMessage; @@ -717,7 +717,7 @@ private void SendInVoiceRange(ChatChannel channel, string name, string message, // If the channel does not support languages, or the entity can understand the message, send the original message, otherwise send the obfuscated version - if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language)) + if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language.ID)) { _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); } @@ -846,7 +846,7 @@ public string WrapPublicMessage(EntityUid source, string name, string message) ("verb", verbName), ("fontType", speech.FontId), ("fontSize", speech.FontSize), - ("message", FormattedMessage.EscapeText(message))); + ("message", message)); } /// diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index da16529d515..8d5a583f6d8 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -1,13 +1,17 @@ using System.Linq; using Content.Server.Ghost.Roles.Components; +using Content.Server.Language; +using Content.Server.Language.Events; using Content.Server.Speech.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.Language; using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Robust.Shared.Prototypes; -using Content.Server.Psionics; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic. -using Content.Shared.Humanoid; //Delta-V - Banning humanoids from becoming ghost roles. +using Content.Server.Psionics; +using Content.Shared.Body.Part; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic. +using Content.Shared.Humanoid; +using Content.Shared.Language.Components; //Delta-V - Banning humanoids from becoming ghost roles. using Content.Shared.Language.Events; namespace Content.Server.Chemistry.ReagentEffects; @@ -28,19 +32,18 @@ public override void Effect(ReagentEffectArgs args) entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); + // Make sure the entity knows at least fallback (Galactic Common) var speaker = entityManager.EnsureComponent(uid); + var knowledge = entityManager.EnsureComponent(uid); var fallback = SharedLanguageSystem.FallbackLanguagePrototype; - if (!speaker.UnderstoodLanguages.Contains(fallback)) - speaker.UnderstoodLanguages.Add(fallback); + if (!knowledge.UnderstoodLanguages.Contains(fallback)) + knowledge.UnderstoodLanguages.Add(fallback); - if (!speaker.SpokenLanguages.Contains(fallback)) - { - speaker.CurrentLanguage = fallback; - speaker.SpokenLanguages.Add(fallback); - } + if (!knowledge.SpokenLanguages.Contains(fallback)) + knowledge.SpokenLanguages.Add(fallback); - args.EntityManager.EventBus.RaiseLocalEvent(uid, new LanguagesUpdateEvent(), true); + IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid, speaker); // Stops from adding a ghost role to things like people who already have a mind if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind) diff --git a/Content.Server/Language/Commands/ListLanguagesCommand.cs b/Content.Server/Language/Commands/ListLanguagesCommand.cs index 6698e1b6453..e5787cba48c 100644 --- a/Content.Server/Language/Commands/ListLanguagesCommand.cs +++ b/Content.Server/Language/Commands/ListLanguagesCommand.cs @@ -1,5 +1,5 @@ -using System.Linq; using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -30,10 +30,29 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) } var languages = IoCManager.Resolve().GetEntitySystem(); + var currentLang = languages.GetLanguage(playerEntity).ID; - var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); + shell.WriteLine(Loc.GetString("command-language-spoken")); + var spoken = languages.GetSpokenLanguages(playerEntity); + for (int i = 0; i < spoken.Count; i++) + { + var lang = spoken[i]; + shell.WriteLine(lang == currentLang + ? Loc.GetString("command-language-current-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang))) + : Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } - shell.WriteLine("Spoken:\n" + string.Join("\n", spokenLangs)); - shell.WriteLine("Understood:\n" + string.Join("\n", knownLangs)); + shell.WriteLine(Loc.GetString("command-language-understood")); + var understood = languages.GetUnderstoodLanguages(playerEntity); + for (int i = 0; i < understood.Count; i++) + { + var lang = understood[i]; + shell.WriteLine(Loc.GetString("command-language-entry", ("id", i + 1), ("language", lang), ("name", LanguageName(lang)))); + } + } + + private string LanguageName(string id) + { + return Loc.GetString($"language-{id}-name"); } } diff --git a/Content.Server/Language/Commands/SayLanguageCommand.cs b/Content.Server/Language/Commands/SayLanguageCommand.cs index 2e4a27b1dcc..2304781fa04 100644 --- a/Content.Server/Language/Commands/SayLanguageCommand.cs +++ b/Content.Server/Language/Commands/SayLanguageCommand.cs @@ -32,7 +32,6 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length < 2) return; - var languageId = args[0]; var message = string.Join(" ", args, startIndex: 1, count: args.Length - 1).Trim(); if (string.IsNullOrEmpty(message)) @@ -41,10 +40,9 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) var languages = IoCManager.Resolve().GetEntitySystem(); var chats = IoCManager.Resolve().GetEntitySystem(); - var language = languages.GetLanguagePrototype(languageId); - if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + if (!SelectLanguageCommand.TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } diff --git a/Content.Server/Language/Commands/SelectLanguageCommand.cs b/Content.Server/Language/Commands/SelectLanguageCommand.cs index e3363846539..d340135925d 100644 --- a/Content.Server/Language/Commands/SelectLanguageCommand.cs +++ b/Content.Server/Language/Commands/SelectLanguageCommand.cs @@ -1,5 +1,7 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Administration; +using Content.Shared.Language; using Robust.Shared.Console; using Robust.Shared.Enums; @@ -32,17 +34,55 @@ public void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length < 1) return; - var languageId = args[0]; - var languages = IoCManager.Resolve().GetEntitySystem(); - var language = languages.GetLanguagePrototype(languageId); - if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + if (!TryParseLanguageArgument(languages, playerEntity, args[0], out var failReason, out var language)) { - shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + shell.WriteError(failReason); return; } languages.SetLanguage(playerEntity, language.ID); } + + // TODO: find a better place for this method + /// + /// Tries to parse the input argument as either a language ID or the position of the language in the list of languages + /// the entity can speak. Returns true if sucessful. + /// + public static bool TryParseLanguageArgument( + LanguageSystem languageSystem, + EntityUid speaker, + string input, + [NotNullWhen(false)] out string? failureReason, + [NotNullWhen(true)] out LanguagePrototype? language) + { + failureReason = null; + language = null; + + if (int.TryParse(input, out var num)) + { + // The argument is a number + var spoken = languageSystem.GetSpokenLanguages(speaker); + if (num > 0 && num - 1 < spoken.Count) + language = languageSystem.GetLanguagePrototype(spoken[num - 1]); + + if (language != null) // the ability to speak it is implied + return true; + + failureReason = Loc.GetString("command-language-invalid-number", ("total", spoken.Count)); + return false; + } + else + { + // The argument is a language ID + language = languageSystem.GetLanguagePrototype(input); + + if (language != null && languageSystem.CanSpeak(speaker, language.ID)) + return true; + + failureReason = Loc.GetString("command-language-invalid-language", ("id", input)); + return false; + } + } } diff --git a/Content.Server/Language/DetermineEntityLanguagesEvent.cs b/Content.Server/Language/DetermineEntityLanguagesEvent.cs index 13ab2cac279..8d6b868d070 100644 --- a/Content.Server/Language/DetermineEntityLanguagesEvent.cs +++ b/Content.Server/Language/DetermineEntityLanguagesEvent.cs @@ -1,29 +1,25 @@ +using Content.Shared.Language; + namespace Content.Server.Language; /// -/// Raised in order to determine the language an entity speaks at the current moment, -/// as well as the list of all languages the entity may speak and understand. +/// Raised in order to determine the list of languages the entity can speak and understand at the given moment. +/// Typically raised on an entity after a language agent (e.g. a translator) has been added to or removed from them. /// -public sealed class DetermineEntityLanguagesEvent : EntityEventArgs +[ByRefEvent] +public record struct DetermineEntityLanguagesEvent { /// - /// The default language of this entity. If empty, remain unchanged. - /// This field has no effect if the entity decides to speak in a concrete language. - /// - public string CurrentLanguage; - /// - /// The list of all languages the entity may speak. Must NOT be held as a reference! + /// The list of all languages the entity may speak. + /// By default, contains the languages this entity speaks intrinsically. /// - public List SpokenLanguages; + public HashSet SpokenLanguages = new(); + /// - /// The list of all languages the entity may understand. Must NOT be held as a reference! + /// The list of all languages the entity may understand. + /// By default, contains the languages this entity understands intrinsically. /// - public List UnderstoodLanguages; + public HashSet UnderstoodLanguages = new(); - public DetermineEntityLanguagesEvent(string currentLanguage, List spokenLanguages, List understoodLanguages) - { - CurrentLanguage = currentLanguage; - SpokenLanguages = spokenLanguages; - UnderstoodLanguages = understoodLanguages; - } + public DetermineEntityLanguagesEvent() {} } diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs index 7517b4185e3..572e2961fde 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -1,5 +1,7 @@ +using Content.Server.Language.Events; using Content.Server.Mind; using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Mind; using Content.Shared.Mind.Components; @@ -7,11 +9,6 @@ namespace Content.Server.Language; -/// -/// LanguageSystem Networking -/// This is used to update client state when mind change entity. -/// - public sealed partial class LanguageSystem { [Dependency] private readonly MindSystem _mind = default!; @@ -19,6 +16,11 @@ public sealed partial class LanguageSystem public void InitializeNet() { + SubscribeNetworkEvent(OnClientSetLanguage); + SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); + + SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); + // Refresh the client's state when its mind hops to a different entity SubscribeLocalEvent((uid, _, _) => SendLanguageStateToClient(uid)); SubscribeLocalEvent((_, _, args) => @@ -26,12 +28,21 @@ public void InitializeNet() if (args.Mind.Comp.Session != null) SendLanguageStateToClient(args.Mind.Comp.Session); }); - - SubscribeLocalEvent((uid, comp, _) => SendLanguageStateToClient(uid, comp)); - SubscribeNetworkEvent((_, session) => SendLanguageStateToClient(session.SenderSession)); } + private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { Valid: true } uid) + return; + + var language = GetLanguagePrototype(message.CurrentLanguage); + if (language == null || !CanSpeak(uid, language.ID)) + return; + + SetLanguage(uid, language.ID); + } + private void SendLanguageStateToClient(EntityUid uid, LanguageSpeakerComponent? comp = null) { // Try to find a mind inside the entity and notify its session @@ -50,10 +61,18 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo SendLanguageStateToClient(entity, session, comp); } + // TODO this is really stupid and can be avoided if we just make everything shared... private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) { - var langs = GetLanguages(uid, component); - var message = new LanguagesUpdatedMessage(langs.CurrentLanguage, langs.SpokenLanguages, langs.UnderstoodLanguages); + var isUniversal = HasComp(uid); + if (!isUniversal) + Resolve(uid, ref component, logMissing: false); + + // I really don't want to call 3 getter methods here, so we'll just have this slightly hardcoded solution + var message = isUniversal || component == null + ? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]) + : new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); + RaiseNetworkEvent(message, session); } } diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index f1bf44c1f4f..e68489e9e28 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -1,289 +1,212 @@ using System.Linq; -using System.Text; -using Content.Server.GameTicking.Events; +using Content.Server.Language.Events; using Content.Shared.Language; +using Content.Shared.Language.Components; using Content.Shared.Language.Events; using Content.Shared.Language.Systems; -using Robust.Shared.Random; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; namespace Content.Server.Language; public sealed partial class LanguageSystem : SharedLanguageSystem { - // Static and re-used event instances used to minimize memory allocations during language processing, which can happen many times per tick. - // These are used in the method GetLanguages and returned from it. They should never be mutated outside of that method or returned outside this system. - private readonly DetermineEntityLanguagesEvent - _determineLanguagesEvent = new(string.Empty, new(), new()), - _universalLanguagesEvent = new(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]); // Returned for universal speakers only - - /// - /// A random number added to each pseudo-random number's seed. Changes every round. - /// - public int RandomRoundSeed { get; private set; } - - public override void Initialize() { base.Initialize(); + InitializeNet(); - SubscribeNetworkEvent(OnClientSetLanguage); SubscribeLocalEvent(OnInitLanguageSpeaker); - SubscribeLocalEvent(_ => RandomRoundSeed = _random.Next()); - - InitializeNet(); } #region public api - /// - /// Obfuscate a message using an entity's default language. - /// - public string ObfuscateSpeech(EntityUid source, string message) - { - var language = GetLanguage(source) ?? Universal; - return ObfuscateSpeech(message, language); - } - /// - /// Obfuscate a message using the given language. - /// - public string ObfuscateSpeech(string message, LanguagePrototype language) + public bool CanUnderstand(EntityUid listener, string language, LanguageSpeakerComponent? component = null) { - var builder = new StringBuilder(); - if (language.ObfuscateSyllables) - ObfuscateSyllables(builder, message, language); - else - ObfuscatePhrases(builder, message, language); + if (language == UniversalPrototype || HasComp(listener)) + return true; - return builder.ToString(); + if (!Resolve(listener, ref component, logMissing: false)) + return false; + + return component.UnderstoodLanguages.Contains(language); } - public bool CanUnderstand(EntityUid listener, LanguagePrototype language, LanguageSpeakerComponent? listenerLanguageComp = null) + public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) { - if (language.ID == UniversalPrototype || HasComp(listener)) + if (HasComp(speaker)) return true; - var listenerLanguages = GetLanguages(listener, listenerLanguageComp)?.UnderstoodLanguages; + if (!Resolve(speaker, ref component, logMissing: false)) + return false; - return listenerLanguages?.Contains(language.ID, StringComparer.Ordinal) ?? false; + return component.SpokenLanguages.Contains(language); } - public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? speakerComp = null) + /// + /// Returns the current language of the given entity, assumes Universal if it's not a language speaker. + /// + public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) { - if (HasComp(speaker)) - return true; + if (HasComp(speaker) || !Resolve(speaker, ref component, logMissing: false)) + return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment) + + if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) + return Universal; - var langs = GetLanguages(speaker, speakerComp)?.UnderstoodLanguages; - return langs?.Contains(language, StringComparer.Ordinal) ?? false; + return proto; } /// - /// Returns the current language of the given entity. - /// Assumes Universal if not specified. + /// Returns the list of languages this entity can speak. /// - public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? languageComp = null) + /// Typically, checking is sufficient. + public List GetSpokenLanguages(EntityUid uid) { - var id = GetLanguages(speaker, languageComp)?.CurrentLanguage; - if (id == null) - return Universal; // Fallback + if (HasComp(uid)) + return [UniversalPrototype]; - _prototype.TryIndex(id, out LanguagePrototype? proto); + if (TryComp(uid, out var component)) + return component.SpokenLanguages; - return proto ?? Universal; + return []; } - public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? languageComp = null) + /// + /// Returns the list of languages this entity can understand. + /// + /// Typically, checking is sufficient. + public List GetUnderstoodLanguages(EntityUid uid) { - if (!CanSpeak(speaker, language) || HasComp(speaker)) - return; + if (HasComp(uid)) + return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one. - if (languageComp == null && !TryComp(speaker, out languageComp)) - return; + if (TryComp(uid, out var component)) + return component.UnderstoodLanguages; + + return []; + } - if (languageComp.CurrentLanguage == language) + public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) + { + if (!CanSpeak(speaker, language) || (HasComp(speaker) && language != UniversalPrototype)) return; - languageComp.CurrentLanguage = language; + if (!Resolve(speaker, ref component) || component.CurrentLanguage == language) + return; + component.CurrentLanguage = language; RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true); } /// - /// Adds a new language to the lists of understood and/or spoken languages of the given component. + /// Adds a new language to the respective lists of intrinsically known languages of the given entity. /// - public void AddLanguage(LanguageSpeakerComponent comp, string language, bool addSpoken = true, bool addUnderstood = true) + public void AddLanguage( + EntityUid uid, + string language, + bool addSpoken = true, + bool addUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - if (addSpoken && !comp.SpokenLanguages.Contains(language)) - comp.SpokenLanguages.Add(language); + if (knowledge == null) + knowledge = EnsureComp(uid); - if (addUnderstood && !comp.UnderstoodLanguages.Contains(language)) - comp.UnderstoodLanguages.Add(language); + if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) + knowledge.SpokenLanguages.Add(language); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } + if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) + knowledge.UnderstoodLanguages.Add(language); - public (List spoken, List understood) GetAllLanguages(EntityUid speaker) - { - var languages = GetLanguages(speaker); - // The lists need to be copied because the internal ones are re-used for performance reasons. - return (new List(languages.SpokenLanguages), new List(languages.UnderstoodLanguages)); + UpdateEntityLanguages(uid, speaker); } /// - /// Ensures the given entity has a valid language as its current language. - /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// Removes a language from the respective lists of intrinsically known languages of the given entity. /// - public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + public void RemoveLanguage( + EntityUid uid, + string language, + bool removeSpoken = true, + bool removeUnderstood = true, + LanguageKnowledgeComponent? knowledge = null, + LanguageSpeakerComponent? speaker = null) { - if (comp == null && !TryComp(entity, out comp)) - return; + if (knowledge == null) + knowledge = EnsureComp(uid); - var langs = GetLanguages(entity, comp); - if (!langs.SpokenLanguages.Contains(comp!.CurrentLanguage, StringComparer.Ordinal)) - { - comp.CurrentLanguage = langs.SpokenLanguages.FirstOrDefault(UniversalPrototype); - RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); - } - } - #endregion + if (removeSpoken) + knowledge.SpokenLanguages.Remove(language); - #region event handling - private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) - { - if (string.IsNullOrEmpty(component.CurrentLanguage)) - component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - } - #endregion + if (removeUnderstood) + knowledge.UnderstoodLanguages.Remove(language); - #region internal api - obfuscation - private void ObfuscateSyllables(StringBuilder builder, string message, LanguagePrototype language) - { - // Go through each word. Calculate its hash sum and count the number of letters. - // Replicate it with pseudo-random syllables of pseudo-random (but similar) length. Use the hash code as the seed. - // This means that identical words will be obfuscated identically. Simple words like "hello" or "yes" in different langs can be memorized. - var wordBeginIndex = 0; - var hashCode = 0; - for (var i = 0; i < message.Length; i++) - { - var ch = char.ToLower(message[i]); - // A word ends when one of the following is found: a space, a sentence end, or EOM - if (char.IsWhiteSpace(ch) || IsSentenceEnd(ch) || i == message.Length - 1) - { - var wordLength = i - wordBeginIndex; - if (wordLength > 0) - { - var newWordLength = PseudoRandomNumber(hashCode, 1, 4); - - for (var j = 0; j < newWordLength; j++) - { - var index = PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count); - builder.Append(language.Replacement[index]); - } - } - - builder.Append(ch); - hashCode = 0; - wordBeginIndex = i + 1; - } - else - hashCode = hashCode * 31 + ch; - } + UpdateEntityLanguages(uid, speaker); } - private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) + /// + /// Ensures the given entity has a valid language as its current language. + /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// + /// True if the current language was modified, false otherwise. + public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) { - // In a similar manner, each phrase is obfuscated with a random number of conjoined obfuscation phrases. - // However, the number of phrases depends on the number of characters in the original phrase. - var sentenceBeginIndex = 0; - for (var i = 0; i < message.Length; i++) + if (!Resolve(entity, ref comp)) + return false; + + if (!comp.SpokenLanguages.Contains(comp.CurrentLanguage)) { - var ch = char.ToLower(message[i]); - if (IsSentenceEnd(ch) || i == message.Length - 1) - { - var length = i - sentenceBeginIndex; - if (length > 0) - { - var newLength = (int) Math.Clamp(Math.Cbrt(length) - 1, 1, 4); // 27+ chars for 2 phrases, 64+ for 3, 125+ for 4. - - for (var j = 0; j < newLength; j++) - { - var phrase = _random.Pick(language.Replacement); - builder.Append(phrase); - } - } - sentenceBeginIndex = i + 1; - - if (IsSentenceEnd(ch)) - builder.Append(ch).Append(" "); - } + comp.CurrentLanguage = comp.SpokenLanguages.FirstOrDefault(UniversalPrototype); + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); + return true; } - } - private static bool IsSentenceEnd(char ch) - { - return ch is '.' or '!' or '?'; + return false; } - #endregion - #region internal api - misc /// - /// Dynamically resolves the current language of the entity and the list of all languages it speaks. - /// - /// If the entity is not a language speaker, or is a universal language speaker, then it's assumed to speak Universal, - /// aka all languages at once and none at the same time. + /// Immediately refreshes the cached lists of spoken and understood languages for the given entity. /// - /// - /// The returned event is reused and thus must not be held as a reference anywhere but inside the caller function. - /// - private DetermineEntityLanguagesEvent GetLanguages(EntityUid speaker, LanguageSpeakerComponent? comp = null) + public void UpdateEntityLanguages(EntityUid entity, LanguageSpeakerComponent? languages = null) { - // This is a shortcut for ghosts and entities that should not speak normally (admemes) - if (HasComp(speaker) || !TryComp(speaker, out comp)) - return _universalLanguagesEvent; + if (!Resolve(entity, ref languages)) + return; - var ev = _determineLanguagesEvent; - ev.SpokenLanguages.Clear(); - ev.UnderstoodLanguages.Clear(); + var ev = new DetermineEntityLanguagesEvent(); + // We add the intrinsically known languages first so other systems can manipulate them easily + if (TryComp(entity, out var knowledge)) + { + foreach (var spoken in knowledge.SpokenLanguages) + ev.SpokenLanguages.Add(spoken); - ev.CurrentLanguage = comp.CurrentLanguage; - ev.SpokenLanguages.AddRange(comp.SpokenLanguages); - ev.UnderstoodLanguages.AddRange(comp.UnderstoodLanguages); + foreach (var understood in knowledge.UnderstoodLanguages) + ev.UnderstoodLanguages.Add(understood); + } - RaiseLocalEvent(speaker, ev, true); + RaiseLocalEvent(entity, ref ev); - if (ev.CurrentLanguage.Length == 0) - ev.CurrentLanguage = !string.IsNullOrEmpty(comp.CurrentLanguage) ? comp.CurrentLanguage : UniversalPrototype; // Fall back to account for admemes like admins possessing a bread - return ev; - } + languages.SpokenLanguages.Clear(); + languages.UnderstoodLanguages.Clear(); - /// - /// Generates a stable pseudo-random number in the range (min, max) for the given seed. - /// Each input seed corresponds to exactly one random number. - /// - private int PseudoRandomNumber(int seed, int min, int max) - { - // This is not a uniform distribution, but it shouldn't matter given there's 2^31 possible random numbers, - // the bias of this function should be so tiny it will never be noticed. - seed += RandomRoundSeed; - var random = ((seed * 1103515245) + 12345) & 0x7fffffff; // Source: http://cs.uccs.edu/~cs591/bufferOverflow/glibc-2.2.4/stdlib/random_r.c - return random % (max - min) + min; + languages.SpokenLanguages.AddRange(ev.SpokenLanguages); + languages.UnderstoodLanguages.AddRange(ev.UnderstoodLanguages); + + if (!EnsureValidLanguage(entity)) + RaiseLocalEvent(entity, new LanguagesUpdateEvent()); } - /// - /// Set CurrentLanguage of the client, the client must be able to Understand the language requested. - /// - private void OnClientSetLanguage(LanguagesSetMessage message, EntitySessionEventArgs args) - { - if (args.SenderSession.AttachedEntity is not {Valid: true} speaker) - return; + #endregion - var language = GetLanguagePrototype(message.CurrentLanguage); + #region event handling - if (language == null || !CanSpeak(speaker, language.ID)) - return; + private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + { + if (string.IsNullOrEmpty(component.CurrentLanguage)) + component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - SetLanguage(speaker, language.ID); + UpdateEntityLanguages(uid, component); } + #endregion } diff --git a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs b/Content.Server/Language/LanguagesUpdateEvent.cs similarity index 78% rename from Content.Shared/Language/Events/LanguagesUpdateEvent.cs rename to Content.Server/Language/LanguagesUpdateEvent.cs index 90ce2f4446b..88ea09916bb 100644 --- a/Content.Shared/Language/Events/LanguagesUpdateEvent.cs +++ b/Content.Server/Language/LanguagesUpdateEvent.cs @@ -1,4 +1,4 @@ -namespace Content.Shared.Language.Events; +namespace Content.Server.Language.Events; /// /// Raised on an entity when its list of languages changes. diff --git a/Content.Server/Language/TranslatorImplantSystem.cs b/Content.Server/Language/TranslatorImplantSystem.cs new file mode 100644 index 00000000000..4d58144481d --- /dev/null +++ b/Content.Server/Language/TranslatorImplantSystem.cs @@ -0,0 +1,66 @@ +using Content.Shared.Implants.Components; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Robust.Shared.Containers; + +namespace Content.Server.Language; + +public sealed class TranslatorImplantSystem : EntitySystem +{ + [Dependency] private readonly LanguageSystem _language = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnImplant); + SubscribeLocalEvent(OnDeImplant); + SubscribeLocalEvent(OnDetermineLanguages); + } + + private void OnImplant(EntityUid uid, TranslatorImplantComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != ImplanterComponent.ImplantSlotId) + return; + + var implantee = Transform(uid).ParentUid; + if (implantee is not { Valid: true } || !TryComp(implantee, out var knowledge)) + return; + + component.Enabled = true; + // To operate an implant, you need to know its required language intrinsically, because like... it connects to your brain or something. + // So external translators or other implants can't help you operate it. + component.SpokenRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.SpokenLanguages, component.RequiresAllLanguages); + + component.UnderstoodRequirementSatisfied = TranslatorSystem.CheckLanguagesMatch( + component.RequiredLanguages, knowledge.UnderstoodLanguages, component.RequiresAllLanguages); + + _language.UpdateEntityLanguages(implantee); + } + + private void OnDeImplant(EntityUid uid, TranslatorImplantComponent component, EntGotRemovedFromContainerMessage args) + { + // Even though the description of this event says it gets raised BEFORE reparenting, that's actually false... + component.Enabled = component.SpokenRequirementSatisfied = component.UnderstoodRequirementSatisfied = false; + + if (TryComp(uid, out var subdermal) && subdermal.ImplantedEntity is { Valid: true} implantee) + _language.UpdateEntityLanguages(implantee); + } + + private void OnDetermineLanguages(EntityUid uid, ImplantedComponent component, ref DetermineEntityLanguagesEvent args) + { + // TODO: might wanna find a better solution, i just can't come up with something viable + foreach (var implant in component.ImplantContainer.ContainedEntities) + { + if (!TryComp(implant, out var translator) || !translator.Enabled) + continue; + + if (translator.SpokenRequirementSatisfied) + foreach (var language in translator.SpokenLanguages) + args.SpokenLanguages.Add(language); + + if (translator.UnderstoodRequirementSatisfied) + foreach (var language in translator.UnderstoodLanguages) + args.UnderstoodLanguages.Add(language); + } + } +} diff --git a/Content.Server/Language/TranslatorImplanterSystem.cs b/Content.Server/Language/TranslatorImplanterSystem.cs deleted file mode 100644 index 1e0c13375e4..00000000000 --- a/Content.Server/Language/TranslatorImplanterSystem.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Linq; -using Content.Server.Administration.Logs; -using Content.Server.Popups; -using Content.Shared.Database; -using Content.Shared.Interaction; -using Content.Shared.Language; -using Content.Shared.Language.Components; -using Content.Shared.Language.Events; -using Content.Shared.Language.Systems; -using Content.Shared.Mobs.Components; -using Content.Shared.Language.Components.Translators; - -namespace Content.Server.Language; - -public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly LanguageSystem _language = default!; - - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnImplant); - } - - - private void OnImplant(EntityUid implanter, TranslatorImplanterComponent component, AfterInteractEvent args) - { - if (component.Used || !args.CanReach || args.Target is not { Valid: true } target) - return; - - if (!TryComp(target, out var speaker)) - return; - - if (component.MobsOnly && !HasComp(target)) - { - _popup.PopupEntity("translator-implanter-refuse", component.Owner); - return; - } - - var understood = _language.GetAllLanguages(target).understood; - if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) - { - _popup.PopupEntity(Loc.GetString("translator-implanter-refuse", - ("implanter", implanter), ("target", target)), implanter); - return; - } - - var intrinsic = EnsureComp(target); - intrinsic.Enabled = true; - - foreach (var lang in component.SpokenLanguages.Where(lang => !intrinsic.SpokenLanguages.Contains(lang))) - intrinsic.SpokenLanguages.Add(lang); - - foreach (var lang in component.UnderstoodLanguages.Where(lang => !intrinsic.UnderstoodLanguages.Contains(lang))) - intrinsic.UnderstoodLanguages.Add(lang); - - component.Used = true; - _popup.PopupEntity(Loc.GetString("translator-implanter-success", - ("implanter", implanter), ("target", target)), implanter); - - _adminLogger.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(implanter):implanter} to give {ToPrettyString(target):target} the following languages:" - + $"\nSpoken: {string.Join(", ", component.SpokenLanguages)}; Understood: {string.Join(", ", component.UnderstoodLanguages)}"); - - OnAppearanceChange(implanter, component); - RaiseLocalEvent(target, new LanguagesUpdateEvent(), true); - } -} diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 3b7704b9a71..5022e540960 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.Language.Events; using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Interaction; @@ -8,6 +9,7 @@ using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; +using Robust.Shared.Utility; namespace Content.Server.Language; @@ -23,7 +25,6 @@ public override void Initialize() { base.Initialize(); - // I wanna die. But my death won't help us discover polymorphism. SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); SubscribeLocalEvent(OnDetermineLanguages); @@ -31,67 +32,36 @@ public override void Initialize() SubscribeLocalEvent(OnTranslatorToggle); SubscribeLocalEvent(OnPowerCellSlotEmpty); - // TODO: why does this use InteractHandEvent?? SubscribeLocalEvent(OnTranslatorInteract); SubscribeLocalEvent(OnTranslatorDropped); } - private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, - DetermineEntityLanguagesEvent ev) + private void OnDetermineLanguages(EntityUid uid, IntrinsicTranslatorComponent component, DetermineEntityLanguagesEvent ev) { - if (!component.Enabled) + if (!component.Enabled || !TryComp(uid, out var speaker)) return; if (!_powerCell.HasActivatableCharge(uid)) return; - var addUnderstood = true; - var addSpoken = true; - if (component.RequiredLanguages.Count > 0) - { - if (component.RequiresAllLanguages) - { - // Add langs when the wielder has all of the required languages - foreach (var language in component.RequiredLanguages) - { - if (!ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) - addSpoken = false; - - if (!ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) - addUnderstood = false; - } - } - else - { - // Add langs when the wielder has at least one of the required languages - addUnderstood = false; - addSpoken = false; - foreach (var language in component.RequiredLanguages) - { - if (ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) - addSpoken = true; - - if (ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) - addUnderstood = true; - } - } - } + // The idea here is as follows: + // Required languages are languages that are required to operate the translator. + // The translator has a limited number of languages it can translate to and translate from. + // If the wielder understands the language of the translator, they will be able to understand translations provided by it + // If the wielder also speaks that language, they will be able to use it to translate their own speech by "speaking" in that language + var addSpoken = CheckLanguagesMatch(component.RequiredLanguages, speaker.SpokenLanguages, component.RequiresAllLanguages); + var addUnderstood = CheckLanguagesMatch(component.RequiredLanguages, speaker.UnderstoodLanguages, component.RequiresAllLanguages); if (addSpoken) - { foreach (var language in component.SpokenLanguages) - AddIfNotExists(ev.SpokenLanguages, language); - - if (component.DefaultLanguageOverride != null && ev.CurrentLanguage.Length == 0) - ev.CurrentLanguage = component.DefaultLanguageOverride; - } + ev.SpokenLanguages.Add(language); if (addUnderstood) foreach (var language in component.UnderstoodLanguages) - AddIfNotExists(ev.UnderstoodLanguages, language); + ev.UnderstoodLanguages.Add(language); } - private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) + private void OnTranslatorInteract(EntityUid translator, HandheldTranslatorComponent component, InteractHandEvent args) { var holder = args.User; if (!EntityManager.HasComponent(holder)) @@ -100,7 +70,7 @@ private void OnTranslatorInteract( EntityUid translator, HandheldTranslatorCompo var intrinsic = EnsureComp(holder); UpdateBoundIntrinsicComp(component, intrinsic, component.Enabled); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder); } private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorComponent component, DroppedEvent args) @@ -115,59 +85,63 @@ private void OnTranslatorDropped(EntityUid translator, HandheldTranslatorCompone RemCompDeferred(holder, intrinsic); } - _language.EnsureValidLanguage(holder); - - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder); } - private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent component, ActivateInWorldEvent args) + private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent translatorComp, ActivateInWorldEvent args) { - if (!component.ToggleOnInteract) + if (!translatorComp.ToggleOnInteract) return; + // This will show a popup if false var hasPower = _powerCell.HasDrawCharge(translator); if (Transform(args.Target).ParentUid is { Valid: true } holder - && EntityManager.HasComponent(holder)) + && TryComp(holder, out var languageComp)) { // This translator is held by a language speaker and thus has an intrinsic counterpart bound to it. // Make sure it's up-to-date. var intrinsic = EnsureComp(holder); - var isEnabled = !component.Enabled; - if (intrinsic.Issuer != component) + var isEnabled = !translatorComp.Enabled; + if (intrinsic.Issuer != translatorComp) { - // The intrinsic comp wasn't owned by this handheld component, so this comp wasn't the active translator. - // Thus it needs to be turned on regardless of its previous state. - intrinsic.Issuer = component; + // The intrinsic comp wasn't owned by this handheld translator, so this wasn't the active translator. + // Thus, the intrinsic comp needs to be turned on regardless of its previous state. + intrinsic.Issuer = translatorComp; isEnabled = true; } - isEnabled &= hasPower; - UpdateBoundIntrinsicComp(component, intrinsic, isEnabled); - component.Enabled = isEnabled; + + UpdateBoundIntrinsicComp(translatorComp, intrinsic, isEnabled); + translatorComp.Enabled = isEnabled; _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); - _language.EnsureValidLanguage(holder); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + // The first new spoken language added by this translator, or null + var firstNewLanguage = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); + + _language.UpdateEntityLanguages(holder, languageComp); + + // Update the current language of the entity if necessary + if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {}) + _language.SetLanguage(holder, firstNewLanguage, languageComp); } else { // This is a standalone translator (e.g. lying on the ground), toggle its state. - component.Enabled = !component.Enabled && hasPower; - _powerCell.SetPowerCellDrawEnabled(translator, !component.Enabled && hasPower); + translatorComp.Enabled = !translatorComp.Enabled && hasPower; + _powerCell.SetPowerCellDrawEnabled(translator, !translatorComp.Enabled && hasPower); } - OnAppearanceChange(translator, component); + OnAppearanceChange(translator, translatorComp); - // HasPower shows a popup when there's no power, so we do not proceed in that case if (hasPower) { var message = Loc.GetString( - component.Enabled + translatorComp.Enabled ? "translator-component-turnon" : "translator-component-shutoff", - ("translator", component.Owner)); - _popup.PopupEntity(message, component.Owner, args.User); + ("translator", translatorComp.Owner)); + _popup.PopupEntity(message, translatorComp.Owner, args.User); } } @@ -178,7 +152,7 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon OnAppearanceChange(translator, component); if (Transform(translator).ParentUid is { Valid: true } holder - && EntityManager.HasComponent(holder)) + && TryComp(holder, out var languageComp)) { if (!EntityManager.TryGetComponent(holder, out var intrinsic)) return; @@ -186,11 +160,10 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon if (intrinsic.Issuer == component) { intrinsic.Enabled = false; - EntityManager.RemoveComponent(holder, intrinsic); + RemComp(holder, intrinsic); } - _language.EnsureValidLanguage(holder); - RaiseLocalEvent(holder, new LanguagesUpdateEvent(), true); + _language.UpdateEntityLanguages(holder, languageComp); } } @@ -201,25 +174,29 @@ private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTra { if (isEnabled) { - intrinsic.SpokenLanguages = new List(comp.SpokenLanguages); - intrinsic.UnderstoodLanguages = new List(comp.UnderstoodLanguages); - intrinsic.DefaultLanguageOverride = comp.DefaultLanguageOverride; + intrinsic.SpokenLanguages = [..comp.SpokenLanguages]; + intrinsic.UnderstoodLanguages = [..comp.UnderstoodLanguages]; } else { intrinsic.SpokenLanguages.Clear(); intrinsic.UnderstoodLanguages.Clear(); - intrinsic.DefaultLanguageOverride = null; } intrinsic.Enabled = isEnabled; intrinsic.Issuer = comp; } - private static void AddIfNotExists(List list, string item) + /// + /// Checks whether any OR all required languages are provided. Used for utility purposes. + /// + public static bool CheckLanguagesMatch(ICollection required, ICollection provided, bool requireAll) { - if (list.Contains(item)) - return; - list.Add(item); + if (required.Count == 0) + return true; + + return requireAll + ? required.All(provided.Contains) + : required.Any(provided.Contains); } } diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index cacd499ab8d..b58d782d9c5 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -61,10 +61,11 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo var language = IoCManager.Resolve().GetEntitySystem(); var speaker = entityManager.EnsureComponent(uid); - // If the speaker knows any language (like monkey or robot), they keep those - // Otherwise, we give them the fallback + + // If the entity already speaks some language (like monkey or robot), we do nothing else + // Otherwise, we give them the fallback language if (speaker.SpokenLanguages.Count == 0) - language.AddLanguage(speaker, SharedLanguageSystem.FallbackLanguagePrototype); + language.AddLanguage(uid, SharedLanguageSystem.FallbackLanguagePrototype); } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 53517da6cb4..2500138a238 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -106,7 +106,7 @@ private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref Rad var parent = Transform(uid).ParentUid; if (TryComp(parent, out ActorComponent? actor)) { - var canUnderstand = _language.CanUnderstand(parent, args.Language); + var canUnderstand = _language.CanUnderstand(parent, args.Language.ID); var msg = new MsgChatMessage { Message = canUnderstand ? args.OriginalChatMsg : args.LanguageObfuscatedChatMsg diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 60aa7c2f4fb..7ed7574a9ae 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -60,7 +60,7 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c // Einstein-Engines - languages mechanic var listener = component.Owner; var msg = args.OriginalChatMsg; - if (listener != null && !_language.CanUnderstand(listener, args.Language)) + if (listener != null && !_language.CanUnderstand(listener, args.Language.ID)) msg = args.LanguageObfuscatedChatMsg; _netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.Channel); @@ -183,7 +183,7 @@ private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), - ("message", FormattedMessage.EscapeText(message))); + ("message", message)); } /// diff --git a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs new file mode 100644 index 00000000000..0632f5d9cb2 --- /dev/null +++ b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// Stores data about entities' intrinsic language knowledge. +/// +[RegisterComponent] +public sealed partial class LanguageKnowledgeComponent : Component +{ + /// + /// List of languages this entity can speak without any external tools. + /// + [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List SpokenLanguages = new(); + + /// + /// List of languages this entity can understand without any external tools. + /// + [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List UnderstoodLanguages = new(); +} diff --git a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs index 95232ffe6ff..e8ebccb3ddf 100644 --- a/Content.Shared/Language/Components/LanguageSpeakerComponent.cs +++ b/Content.Shared/Language/Components/LanguageSpeakerComponent.cs @@ -1,29 +1,32 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - namespace Content.Shared.Language; -[RegisterComponent, AutoGenerateComponentState] +// TODO: either move all language speaker-related components to server side, or make everything else shared. +// The current approach leads to confusion, as the server never informs the client of updates in these components. + +/// +/// Stores the current state of the languages the entity can speak and understand. +/// +/// +/// All fields of this component are populated during a DetermineEntityLanguagesEvent. +/// They are not to be modified externally. +/// +[RegisterComponent] public sealed partial class LanguageSpeakerComponent : Component { /// - /// The current language the entity may use to speak. + /// The current language the entity uses when speaking. /// Other listeners will hear the entity speak in this language. /// - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public string CurrentLanguage = default!; + [DataField] + public string CurrentLanguage = ""; // The language system will override it on init /// - /// List of languages this entity can speak. + /// List of languages this entity can speak at the current moment. /// - [ViewVariables] - [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List SpokenLanguages = new(); + public List SpokenLanguages = []; /// - /// List of languages this entity can understand. + /// List of languages this entity can understand at the current moment. /// - [ViewVariables] - [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] - public List UnderstoodLanguages = new(); + public List UnderstoodLanguages = []; } diff --git a/Content.Shared/Language/Components/TranslatorImplantComponent.cs b/Content.Shared/Language/Components/TranslatorImplantComponent.cs new file mode 100644 index 00000000000..cb8c666c82f --- /dev/null +++ b/Content.Shared/Language/Components/TranslatorImplantComponent.cs @@ -0,0 +1,21 @@ +using Content.Shared.Language.Components.Translators; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// An implant that allows the implantee to speak and understand other languages. +/// +[RegisterComponent] +public sealed partial class TranslatorImplantComponent : BaseTranslatorComponent +{ + /// + /// Whether the implantee knows the languages necessary to speak using this implant. + /// + public bool SpokenRequirementSatisfied = false; + + /// + /// Whether the implantee knows the languages necessary to understand translations of this implant. + /// + public bool UnderstoodRequirementSatisfied = false; +} diff --git a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs b/Content.Shared/Language/Components/TranslatorImplanterComponent.cs deleted file mode 100644 index 401e8a8b8aa..00000000000 --- a/Content.Shared/Language/Components/TranslatorImplanterComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Language.Components; - -/// -/// An item that, when used on a mob, adds an intrinsic translator to it. -/// -[RegisterComponent] -public sealed partial class TranslatorImplanterComponent : Component -{ - [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List SpokenLanguages = new(); - - [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List UnderstoodLanguages = new(); - - /// - /// The list of languages the mob must understand in order for this translator to have effect. - /// Knowing one language is enough. - /// - [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer))] - public List RequiredLanguages = new(); - - /// - /// If true, only allows to use this implanter on mobs. - /// - [DataField] - public bool MobsOnly = true; - - /// - /// Whether this implant has been used already. - /// - [DataField] - public bool Used = false; -} diff --git a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs index a66c9be082e..072480031d5 100644 --- a/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/BaseTranslatorComponent.cs @@ -4,15 +4,6 @@ namespace Content.Shared.Language.Components.Translators; public abstract partial class BaseTranslatorComponent : Component { - // TODO may need to be removed completely, it's a part of legacy code that never ended up being used. - /// - /// The language this translator changes the speaker's language to when they don't specify one. - /// If null, does not modify the default language. - /// - [DataField("defaultLanguage")] - [ViewVariables(VVAccess.ReadWrite)] - public string? DefaultLanguageOverride = null; - /// /// The list of additional languages this translator allows the wielder to speak. /// diff --git a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs index f900603f01d..7e3de0eca61 100644 --- a/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs +++ b/Content.Shared/Language/Components/Translators/HandheldTranslatorComponent.cs @@ -7,9 +7,18 @@ namespace Content.Shared.Language.Components.Translators; public sealed partial class HandheldTranslatorComponent : Translators.BaseTranslatorComponent { /// - /// Whether or not interacting with this translator - /// toggles it on or off. + /// Whether interacting with this translator toggles it on and off. /// - [DataField("toggleOnInteract")] + [DataField] public bool ToggleOnInteract = true; + + /// + /// If true, when this translator is turned on, the entities' current spoken language will be set + /// to the first new language added by this translator. + /// + /// + /// This should generally be used for translators that translate speech between two languages. + /// + [DataField] + public bool SetLanguageOnInteract = true; } diff --git a/Content.Shared/Language/LanguagePrototype.cs b/Content.Shared/Language/LanguagePrototype.cs index 801ab8a393b..9342c07e91f 100644 --- a/Content.Shared/Language/LanguagePrototype.cs +++ b/Content.Shared/Language/LanguagePrototype.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using Robust.Shared.Prototypes; namespace Content.Shared.Language; @@ -10,18 +9,10 @@ public sealed class LanguagePrototype : IPrototype public string ID { get; private set; } = default!; /// - /// If true, obfuscated phrases of creatures speaking this language will have their syllables replaced with "replacement" syllables. - /// Otherwise entire sentences will be replaced. + /// Obfuscation method used by this language. By default, uses /// - [DataField(required: true)] - public bool ObfuscateSyllables; - - /// - /// Lists all syllables that are used to obfuscate a message a listener cannot understand if obfuscateSyllables is true. - /// Otherwise uses all possible phrases the creature can make when trying to say anything. - /// - [DataField(required: true)] - public List Replacement = []; + [DataField("obfuscation")] + public ObfuscationMethod Obfuscation = ObfuscationMethod.Default; #region utility /// diff --git a/Content.Shared/Language/ObfuscationMethods.cs b/Content.Shared/Language/ObfuscationMethods.cs new file mode 100644 index 00000000000..51230c47970 --- /dev/null +++ b/Content.Shared/Language/ObfuscationMethods.cs @@ -0,0 +1,184 @@ +using System.Text; +using Content.Shared.Language.Systems; + +namespace Content.Shared.Language; + +[ImplicitDataDefinitionForInheritors] +public abstract partial class ObfuscationMethod +{ + /// + /// The fallback obfuscation method, replaces the message with the string "<?>". + /// + public static readonly ObfuscationMethod Default = new ReplacementObfuscation + { + Replacement = new List { "" } + }; + + /// + /// Obfuscates the provided message and writes the result into the provided StringBuilder. + /// Implementations should use the context's pseudo-random number generator and provide stable obfuscations. + /// + internal abstract void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context); + + /// + /// Obfuscates the provided message. This method should only be used for debugging purposes. + /// For all other purposes, use instead. + /// + public string Obfuscate(string message) + { + var builder = new StringBuilder(); + Obfuscate(builder, message, IoCManager.Resolve().GetEntitySystem()); + return builder.ToString(); + } +} + +/// +/// The most primitive method of obfuscation - replaces the entire message with one random replacement phrase. +/// Similar to ReplacementAccent. Base for all replacement-based obfuscation methods. +/// +public partial class ReplacementObfuscation : ObfuscationMethod +{ + /// + /// A list of replacement phrases used in the obfuscation process. + /// + [DataField(required: true)] + public List Replacement = []; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var idx = context.PseudoRandomNumber(message.GetHashCode(), 0, Replacement.Count - 1); + builder.Append(Replacement[idx]); + } +} + +/// +/// Obfuscates the provided message by replacing each word with a random number of syllables in the range (min, max), +/// preserving the original punctuation to a resonable extent. +/// +/// +/// The words are obfuscated in a stable manner, such that every particular word will be obfuscated the same way throughout one round. +/// This means that particular words can be memorized within a round, but not across rounds. +/// +public sealed partial class SyllableObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinSyllables = 1; + + [DataField] + public int MaxSyllables = 4; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + const char eof = (char) 0; // Special character to mark the end of the message in the code below + + var wordBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i <= message.Length; i++) + { + var ch = i < message.Length ? char.ToLower(message[i]) : eof; + var isWordEnd = char.IsWhiteSpace(ch) || IsPunctuation(ch) || ch == eof; + + // If this is a normal char, add it to the hash sum + if (!isWordEnd) + hashCode = hashCode * 31 + ch; + + // If a word ends before this character, construct a new word and append it to the new message. + if (isWordEnd) + { + var wordLength = i - wordBeginIndex; + if (wordLength > 0) + { + var newWordLength = context.PseudoRandomNumber(hashCode, MinSyllables, MaxSyllables); + + for (var j = 0; j < newWordLength; j++) + { + var index = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + builder.Append(Replacement[index]); + } + } + + hashCode = 0; + wordBeginIndex = i + 1; + } + + // If this message concludes a word (i.e. is a whitespace or a punctuation mark), append it to the message + if (isWordEnd && ch != eof) + builder.Append(ch); + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?' or ',' or ':'; + } +} + +/// +/// Obfuscates each sentence in the message by concatenating a number of obfuscation phrases. +/// The number of phrases in the obfuscated message is proportional to the length of the original message. +/// +public sealed partial class PhraseObfuscation : ReplacementObfuscation +{ + [DataField] + public int MinPhrases = 1; + + [DataField] + public int MaxPhrases = 4; + + /// + /// A string used to separate individual phrases within one sentence. Default is a space. + /// + [DataField] + public string Separator = " "; + + /// + /// A power to which the number of characters in the original message is raised to determine the number of phrases in the result. + /// Default is 1/3, i.e. the cubic root of the original number. + /// + /// + /// Using the default proportion, you will need at least 27 characters for 2 phrases, at least 64 for 3, at least 125 for 4, etc. + /// Increasing the proportion to 1/4 will result in the numbers changing to 81, 256, 625, etc. + /// + [DataField] + public float Proportion = 1f / 3; + + internal override void Obfuscate(StringBuilder builder, string message, SharedLanguageSystem context) + { + var sentenceBeginIndex = 0; + var hashCode = 0; + + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + if (!IsPunctuation(ch) && i != message.Length - 1) + { + hashCode = hashCode * 31 + ch; + continue; + } + + var length = i - sentenceBeginIndex; + if (length > 0) + { + var newLength = (int) Math.Clamp(Math.Pow(length, Proportion) - 1, MinPhrases, MaxPhrases); + + for (var j = 0; j < newLength; j++) + { + var phraseIdx = context.PseudoRandomNumber(hashCode + j, 0, Replacement.Count - 1); + var phrase = Replacement[phraseIdx]; + builder.Append(phrase); + builder.Append(Separator); + } + } + sentenceBeginIndex = i + 1; + + if (IsPunctuation(ch)) + builder.Append(ch).Append(' '); // TODO: this will turn '...' into '. . . ' + } + } + + private static bool IsPunctuation(char ch) + { + return ch is '.' or '!' or '?'; // Doesn't include mid-sentence punctuation like the comma + } +} diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index e2eeb8bb493..0a03086ebe1 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -1,6 +1,6 @@ -using Content.Shared.Actions; +using System.Text; +using Content.Shared.GameTicking; using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Shared.Language.Systems; @@ -24,7 +24,7 @@ public abstract class SharedLanguageSystem : EntitySystem public static LanguagePrototype Universal { get; private set; } = default!; [Dependency] protected readonly IPrototypeManager _prototype = default!; - [Dependency] protected readonly IRobustRandom _random = default!; + [Dependency] protected readonly SharedGameTicker _ticker = default!; public override void Initialize() { @@ -36,4 +36,32 @@ public override void Initialize() _prototype.TryIndex(id, out var proto); return proto; } + + /// + /// Obfuscate a message using the given language. + /// + public string ObfuscateSpeech(string message, LanguagePrototype language) + { + var builder = new StringBuilder(); + var method = language.Obfuscation; + method.Obfuscate(builder, message, this); + + return builder.ToString(); + } + + /// + /// Generates a stable pseudo-random number in the range (min, max) (inclusively) for the given seed. + /// One seed always corresponds to one number, however the resulting number also depends on the current round number. + /// This method is meant to be used in to provide stable obfuscation. + /// + internal int PseudoRandomNumber(int seed, int min, int max) + { + // Using RobustRandom or System.Random here is a bad idea because this method can get called hundreds of times per message. + // Each call would require us to allocate a new instance of random, which would lead to lots of unnecessary calculations. + // Instead, we use a simple but effective algorithm derived from the C language. + // It does not produce a truly random number, but for the purpose of obfuscating messages in an RP-based game it's more than alright. + seed = seed ^ (_ticker.RoundId * 127); + var random = seed * 1103515245 + 12345; + return min + Math.Abs(random) % (max - min + 1); + } } diff --git a/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs b/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs deleted file mode 100644 index a13225378cd..00000000000 --- a/Content.Shared/Language/Systems/SharedTranslatorImplanterSystem.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Content.Shared.Examine; -using Content.Shared.Implants.Components; -using Content.Shared.Language.Components; -using Robust.Shared.Serialization; - -namespace Content.Shared.Language.Systems; - -public abstract class SharedTranslatorImplanterSystem : EntitySystem -{ - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnExamined); - } - - private void OnExamined(EntityUid uid, TranslatorImplanterComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - - var text = !component.Used - ? Loc.GetString("translator-implanter-ready") - : Loc.GetString("translator-implanter-used"); - - args.PushText(text); - } - - protected void OnAppearanceChange(EntityUid implanter, TranslatorImplanterComponent component) - { - var used = component.Used; - _appearance.SetData(implanter, ImplanterVisuals.Full, !used); - } -} diff --git a/Resources/Locale/en-US/language/commands.ftl b/Resources/Locale/en-US/language/commands.ftl index 32fa5415b8c..ba2b3160094 100644 --- a/Resources/Locale/en-US/language/commands.ftl +++ b/Resources/Locale/en-US/language/commands.ftl @@ -1,8 +1,16 @@ command-list-langs-desc = List languages your current entity can speak at the current moment. command-list-langs-help = Usage: {$command} -command-saylang-desc = Send a message in a specific language. -command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!" +command-saylang-desc = Send a message in a specific language. To choose a language, you can use either the name of the language, or its position in the list of languages. +command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!". Example: {$command} 1 "Hello World!" -command-language-select-desc = Select the currently spoken language of your entity. -command-language-select-help = Usage: {$command} . Example: {$command} GalacticCommon +command-language-select-desc = Select the currently spoken language of your entity. You can use either the name of the language, or its position in the list of languages. +command-language-select-help = Usage: {$command} . Example: {$command} 1. Example: {$command} GalacticCommon + +command-language-spoken = Spoken: +command-language-understood = Understood: +command-language-current-entry = {$id}. {$language} - {$name} (current) +command-language-entry = {$id}. {$language} - {$name} + +command-language-invalid-number = The language number must be between 0 and {$total}. Alternatively, use the language name. +command-language-invalid-language = The language {$id} does not exist or you cannot speak it. diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml index e932974a0f4..dd59d74d3f0 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml @@ -66,7 +66,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -179,7 +179,7 @@ tags: - DoorBumpOpener - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml index fa51b99325c..2dad0fe2e65 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml @@ -95,7 +95,7 @@ factions: - PsionicInterloper - NanoTrasen - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml index c2ae33ec0ba..8968f0e77ad 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml @@ -96,7 +96,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml index e51ae91d12c..9b851ffa522 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml @@ -133,7 +133,7 @@ - FootstepSound - DoorBumpOpener - ShoesRequiredStepTriggerImmune - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml index 52853d696a2..9e4f80bfb52 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml @@ -97,7 +97,7 @@ Female: FemaleVulpkanin Unsexed: MaleVulpkanin - type: DogVision - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Canilunzt diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 0645e451af2..196abcfab76 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -213,7 +213,7 @@ visMask: - PsionicInvisibility - Normal - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 7fe105f940c..ec917607abc 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -49,7 +49,7 @@ flavorKind: station-event-random-sentience-flavor-organic - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -243,7 +243,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Chicken understands: @@ -522,7 +522,7 @@ prob: 0.5 - type: Extractable grindableSolutionName: food - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Moffic understands: @@ -624,7 +624,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Duck understands: @@ -879,7 +879,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Voice/Arachnid/arachnid_chitter.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: @@ -1127,7 +1127,7 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -1321,7 +1321,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1360,7 +1360,7 @@ - type: Speech speechSounds: Monkey speechVerb: Monkey - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -1405,7 +1405,7 @@ - type: NameIdentifier group: Kobold - type: LizardAccent - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kobold understands: @@ -1640,7 +1640,7 @@ spawned: - id: FoodMeatRat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -1989,7 +1989,7 @@ path: /Audio/Animals/parrot_raught.ogg - type: Bloodstream bloodMaxVolume: 50 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon understands: @@ -2255,7 +2255,7 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -2594,7 +2594,7 @@ - type: Tag tags: - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -2646,7 +2646,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -2804,7 +2804,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -2876,7 +2876,7 @@ - type: NpcFactionMember factions: - Syndicate - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -3185,7 +3185,7 @@ spawned: - id: FoodMeat amount: 1 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -3298,7 +3298,7 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Pig understands: @@ -3389,7 +3389,7 @@ reformTime: 10 popupText: diona-reform-attempt reformPrototype: MobDionaReformed - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - RootSpeak understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 3bcf8e7a16f..f8d0497b971 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -15,7 +15,7 @@ - type: Sprite sprite: Mobs/Aliens/Argocyte/argocyte_common.rsi - type: SolutionContainerManager - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 5141811a8ea..6eb43fb89ae 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -36,7 +36,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalIan - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -127,7 +127,7 @@ tags: - CannotSuicide - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -151,7 +151,7 @@ tags: - CannotSuicide - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: @@ -306,7 +306,7 @@ spawned: - id: FoodMeat amount: 2 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -411,7 +411,7 @@ spawned: - id: FoodMeat amount: 3 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Dog understands: @@ -576,7 +576,7 @@ - VimPilot - type: StealTarget stealGroup: AnimalRenault - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Fox understands: @@ -629,7 +629,7 @@ - CannotSuicide - Hamster - VimPilot - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: @@ -807,7 +807,7 @@ attributes: proper: true gender: female - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: @@ -847,7 +847,7 @@ attributes: proper: true gender: male - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Monkey understands: @@ -881,7 +881,7 @@ # - type: AlwaysRevolutionaryConvertible - type: StealTarget stealGroup: AnimalTropico - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Crab understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 31a32333f3f..42da84323f6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -119,7 +119,7 @@ attributes: gender: male - type: PotentialPsionic # Nyano - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Mouse @@ -298,7 +298,7 @@ - type: Food - type: Item size: Tiny # Delta V - Make them eatable and pickable. - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Mouse understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml index 9559ae3a0c0..08cb776c530 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/shadows.yml @@ -50,7 +50,7 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: cat - type: Physics - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Cat understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index e3166c15f6e..f9cf0db5f6f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -107,7 +107,7 @@ - type: TypingIndicator proto: robot - type: ZombieImmune - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index f18b371c4c2..c4d88e80705 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -112,7 +112,7 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Bubblish understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index 9b79d67f408..adf3af93eab 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -165,7 +165,7 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepBounce - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Kangaroo understands: @@ -251,7 +251,7 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 4 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -357,7 +357,7 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 6 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 26553a2f1f2..397989643e6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -125,7 +125,7 @@ chance: -2 - type: Psionic #Nyano - Summary: makes psionic by default. removable: false - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - Xeno understands: @@ -239,7 +239,7 @@ - type: Tag tags: - CannotSuicide - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 67212d416fe..da9858105b4 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -219,7 +219,7 @@ price: 1500 # Kidnapping a living person and selling them for cred is a good move. deathPenalty: 0.01 # However they really ought to be living and intact, otherwise they're worth 100x less. - type: CanEscapeInventory # Carrying system from nyanotrasen. - - type: LanguageSpeaker # This is here so all with no LanguageSpeaker at least spawn with the default languages. + - type: LanguageKnowledge # This is here so even if species doesn't have a defined language, they at least speak GC speaks: - GalacticCommon understands: diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 5cb3de6f168..de9928f0d67 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -102,7 +102,7 @@ actionPrototype: DionaGibAction allowedStates: - Dead - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RootSpeak diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index c5395360187..b38ea2634fd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -51,7 +51,7 @@ accent: dwarf - type: Speech speechSounds: Bass - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 7c3f857c001..e00e06279e5 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -17,7 +17,7 @@ - id: FoodMeatHuman amount: 5 - type: PotentialPsionic #Nyano - Summary: makes potentially psionic. - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 5959c497186..1c55dcf0df1 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -23,7 +23,7 @@ accent: zombieMoth - type: Speech speechVerb: Moth - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Moffic diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index bdea4499ed1..35f9e9fa393 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -59,7 +59,7 @@ types: Heat : 1.5 #per second, scales with temperature & other constants - type: Wagging - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Draconic diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index a601010ef94..6c3dc952957 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -74,7 +74,7 @@ types: Asphyxiation: -1.0 maxSaturation: 15 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Bubblish diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index d5be77eef1c..40cb0da95a1 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -43,6 +43,7 @@ - type: MovementSpeedModifier - type: Polymorphable - type: StatusIcon + - type: LanguageSpeaker # Einstein Engines. This component is required to support speech, although it does not define known languages. - type: RequireProjectileTarget active: False diff --git a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml index fc947efe9a3..da42b2774b1 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml @@ -1,132 +1,128 @@ - type: entity - abstract: true - id: BaseTranslatorImplanter - parent: [ BaseItem ] - name: basic translator implant - description: Translates speech. + parent: BaseSubdermalImplant + id: BasicGalacticCommonTranslatorImplant + name: basic common translator implant + description: Provides your illiterate friends the ability to understand the common galactic tongue. + noSpawn: true components: - - type: Sprite - sprite: Objects/Specific/Medical/implanter.rsi - state: implanter0 - layers: - - state: implanter1 - map: [ "implantFull" ] - visible: true - - state: implanter0 - map: [ "implantBroken" ] - - type: Appearance - - type: GenericVisualizer - visuals: - enum.ImplanterVisuals.Full: - implantFull: - True: {visible: true} - False: {visible: false} - implantBroken: - True: {visible: false} - False: {visible: true} - -- type: entity - id: BasicGalaticCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: basic Galactic Common translator implant - description: An implant giving the ability to understand Galactic Common. - components: - - type: TranslatorImplanter + - type: TranslatorImplant understood: - GalacticCommon - type: entity - id: AdvancedGalaticCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: advanced Galactic Common translator implant - description: An implant giving the ability to understand and speak Galactic Common. + parent: BaseSubdermalImplant + id: GalacticCommonTranslatorImplant + name: advanced common translator implant + description: A more advanced version of the translator implant, teaches your illiterate friends the ability to both speak and understand the galactic tongue! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - GalacticCommon + - type: TranslatorImplant understood: - GalacticCommon + spoken: + - GalacticCommon - type: entity - id: BubblishTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Bubblish translator implant - description: An implant giving the ability to understand and speak Bubblish. + parent: BaseSubdermalImplant + id: BubblishTranslatorImplant + name: bubblish translator implant + description: An implant that helps you speak and understand the language of slimes! Special vocal chords not included. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Bubblish + - type: TranslatorImplant understood: - Bubblish + spoken: + - Bubblish + requires: + - GalacticCommon - type: entity - id: NekomimeticTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Nekomimetic translator implant - description: An implant giving the ability to understand and speak Nekomimetic. Nya~! + parent: BaseSubdermalImplant + id: NekomimeticTranslatorImplant + name: nekomimetic translator implant + description: A translator implant intially designed to help domestic cat owners understand their pets, now granting the ability to understand and speak to Felinids! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Nekomimetic + - type: TranslatorImplant understood: - Nekomimetic + spoken: + - Nekomimetic + requires: + - GalacticCommon - type: entity - id: DraconicTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Draconic translator implant - description: An implant giving the ability to understand and speak Draconic. + parent: BaseSubdermalImplant + id: DraconicTranslatorImplant + name: draconic translator implant + description: A translator implant giving the ability to speak to dragons! Subsequently, also allows to communicate with the Unathi. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Draconic + - type: TranslatorImplant understood: - Draconic + spoken: + - Draconic + requires: + - GalacticCommon - type: entity - id: CanilunztTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: Canilunzt translator implant - description: An implant giving the ability to understand and speak Canilunzt. Yeeps! + parent: BaseSubdermalImplant + id: CanilunztTranslatorImplant + name: canilunzt translator implant + description: A translator implant that helps you communicate with your local yeepers. Yeep! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Canilunzt + - type: TranslatorImplant understood: - Canilunzt + spoken: + - Canilunzt + requires: + - GalacticCommon - type: entity - id: SolCommonTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: SolCommon translator implant + parent: BaseSubdermalImplant + id: SolCommonTranslatorImplant + name: sol-common translator implant description: An implant giving the ability to understand and speak SolCommon. Raaagh! + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - SolCommon + - type: TranslatorImplant understood: - SolCommon + spoken: + - SolCommon + requires: + - GalacticCommon - type: entity - id: RootSpeakTranslatorImplanter - parent: [ BaseTranslatorImplanter ] - name: RootSpeak translator implant - description: An implant giving the ability to understand and speak RootSpeak. + parent: BaseSubdermalImplant + id: RootSpeakTranslatorImplant + name: root-speak translator implant + description: An implant that lets you speak for the trees. Or to the trees. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - RootSpeak + - type: TranslatorImplant understood: - RootSpeak + spoken: + - RootSpeak + requires: + - GalacticCommon - type: entity - id: MofficTranslatorImplanter - parent: [ BaseTranslatorImplanter ] + parent: BaseSubdermalImplant + id: MofficTranslatorImplant name: Moffic translator implant - description: An implant giving the ability to understand and speak Moffic. + description: An implant designed to help domesticate mothroaches. Subsequently, allows you to communicate with the moth people. + noSpawn: true components: - - type: TranslatorImplanter - spoken: - - Moffic + - type: TranslatorImplant understood: - Moffic + spoken: + - Moffic + requires: + - GalacticCommon diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index e5ad824c5d9..664626ea4b4 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -1,7 +1,7 @@ - type: entity abstract: true id: TranslatorUnpowered - parent: [ BaseItem ] + parent: BaseItem name: translator description: Translates speech. components: @@ -36,7 +36,7 @@ - type: entity abstract: true id: TranslatorEmpty - parent: [ Translator ] + parent: Translator suffix: Empty components: - type: ItemSlots @@ -49,7 +49,7 @@ id: CanilunztTranslator parent: [ TranslatorEmpty ] name: Canilunzt translator - description: Translates speech between Canilunzt and Galactic Common. + description: Translates speech between Canilunzt and Galactic Common, allowing your local yeepers to communicate with the locals and vice versa! components: - type: HandheldTranslator spoken: @@ -66,7 +66,7 @@ id: BubblishTranslator parent: [ TranslatorEmpty ] name: Bubblish translator - description: Translates speech between Bubblish and Galactic Common. + description: Translates speech between Bubblish and Galactic Common, helping communicate with slimes and slime people. components: - type: HandheldTranslator spoken: @@ -83,7 +83,7 @@ id: NekomimeticTranslator parent: [ TranslatorEmpty ] name: Nekomimetic translator - description: Translates speech between Nekomimetic and Galactic Common. Why would you want that? + description: Translates speech between Nekomimetic and Galactic Common, enabling you to communicate with your pet cats. components: - type: HandheldTranslator spoken: @@ -100,7 +100,7 @@ id: DraconicTranslator parent: [ TranslatorEmpty ] name: Draconic translator - description: Translates speech between Draconic and Galactic Common. + description: Translates speech between Draconic and Galactic Common, making it easier to understand your local Uniathi. components: - type: HandheldTranslator spoken: @@ -134,7 +134,7 @@ id: RootSpeakTranslator parent: [ TranslatorEmpty ] name: RootSpeak translator - description: Translates speech between RootSpeak and Galactic Common. Like a true plant? + description: Translates speech between RootSpeak and Galactic Common. You may now speak for the trees. components: - type: HandheldTranslator spoken: @@ -151,7 +151,7 @@ id: MofficTranslator parent: [ TranslatorEmpty ] name: Moffic translator - description: Translates speech between Moffic and Galactic Common. Like a true moth... or bug? + description: Translates speech between Moffic and Galactic Common, helping you understand the buzzes of your pet mothroach! components: - type: HandheldTranslator spoken: @@ -168,7 +168,7 @@ id: XenoTranslator parent: [ TranslatorEmpty ] name: Xeno translator - description: Translates speech between Xeno and Galactic Common. Not sure if that will help. + description: Translates speech between Xeno and Galactic Common. This will probably not help you survive an encounter, though. components: - type: HandheldTranslator spoken: @@ -184,7 +184,7 @@ id: AnimalTranslator parent: [ TranslatorEmpty ] name: Animal translator - description: Translates all the cutes noises that animals make into a more understandable form! + description: Translates all the cutes noises that most animals make into a more understandable form! components: - type: HandheldTranslator understood: @@ -203,3 +203,4 @@ - Kobold requires: - GalacticCommon + setLanguageOnInteract: false diff --git a/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml new file mode 100644 index 00000000000..8b5b262ff8a --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml @@ -0,0 +1,77 @@ +- type: entity + id: BaseTranslatorImplanter + abstract: true + parent: BaseImplantOnlyImplanter + name: basic translator implanter + +- type: entity + id: BasicGalaticCommonTranslatorImplanter + parent: BaseTranslatorImplanter + name: basic common translator implanter + components: + - type: Implanter + implant: BasicGalacticCommonTranslatorImplant + +- type: entity + id: AdvancedGalaticCommonTranslatorImplanter + parent: BaseTranslatorImplanter + name: advanced common translator implanter + components: + - type: Implanter + implant: GalacticCommonTranslatorImplant + +- type: entity + id: BubblishTranslatorImplanter + parent: BaseTranslatorImplanter + name: bubblish translator implant + components: + - type: Implanter + implant: BubblishTranslatorImplant + +- type: entity + id: NekomimeticTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: nekomimetic translator implant + components: + - type: Implanter + implant: NekomimeticTranslatorImplant + +- type: entity + id: DraconicTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: draconic translator implant + components: + - type: Implanter + implant: DraconicTranslatorImplant + +- type: entity + id: CanilunztTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: canilunzt translator implant + components: + - type: Implanter + implant: CanilunztTranslatorImplant + +- type: entity + id: SolCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: sol-common translator implant + components: + - type: Implanter + implant: SolCommonTranslatorImplant + +- type: entity + id: RootSpeakTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: root-speak translator implant + components: + - type: Implanter + implant: RootSpeakTranslatorImplant + +- type: entity + id: MofficTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: moffic translator implant + components: + - type: Implanter + implant: MofficTranslatorImplant diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 380f7e012d1..2c4fccb8b3e 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -104,7 +104,7 @@ price: 100 - type: Appearance - type: WiresVisuals - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - RobotTalk diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml index 90bce1baed2..1a874612c2f 100644 --- a/Resources/Prototypes/Language/languages.yml +++ b/Resources/Prototypes/Language/languages.yml @@ -1,493 +1,558 @@ # The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. -# Do not use otherwise. Try to use the respective component instead of this language. +# Do not use otherwise. Making an entity explicitly understand/speak this language will NOT have the desired effect. - type: language id: Universal - obfuscateSyllables: false - replacement: - - "*incomprehensible*" + obfuscation: + !type:ReplacementObfuscation + replacement: + - "*incomprehensible*" # Never actually used # The common galactic tongue. - type: language id: GalacticCommon - obfuscateSyllables: true - replacement: - - Blah - - Blah - - Blah - - dingle-doingle - - dingle - - dangle - - jibber-jabber - - jubber - - bleh - - zippity - - zoop - - wibble - - wobble - - wiggle - - yada - - meh - - neh - - nah - - wah + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - blah + - blah + - blah + - dingle-doingle + - dingle + - dangle + - jibber-jabber + - jubber + - bleh + - zippity + - zoop + - wibble + - wobble + - wiggle + - yada + - meh + - neh + - nah + - wah # Spoken by slimes. - type: language id: Bubblish - obfuscateSyllables: true - replacement: - - blob - - plop - - pop - - bop - - boop + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - blob + - plop + - pop + - bop + - boop # Spoken by moths. - type: language id: Moffic - obfuscateSyllables: true - replacement: - - år - - i - - går - - sek - - mo - - ff - - ok - - gj - - ø - - gå - - la - - le - - lit - - ygg - - van - - dår - - næ - - møt - - idd - - hvo - - ja - - på - - han - - så - - ån - - det - - att - - nå - - gö - - bra - - int - - tyc - - om - - när - - två - - må - - dag - - sjä - - vii - - vuo - - eil - - tun - - käyt - - teh - - vä - - hei - - huo - - suo - - ää - - ten - - ja - - heu - - stu - - uhr - - kön - - we - - hön + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 # Replacements are really short + maxSyllables: 4 + replacement: + - år + - i + - går + - sek + - mo + - ff + - ok + - gj + - ø + - gå + - la + - le + - lit + - ygg + - van + - dår + - næ + - møt + - idd + - hvo + - ja + - på + - han + - så + - ån + - det + - att + - nå + - gö + - bra + - int + - tyc + - om + - när + - två + - må + - dag + - sjä + - vii + - vuo + - eil + - tun + - käyt + - teh + - vä + - hei + - huo + - suo + - ää + - ten + - ja + - heu + - stu + - uhr + - kön + - we + - hön - # Spoken by dionas. +# Spoken by dionas. - type: language id: RootSpeak - obfuscateSyllables: true - replacement: - - hs - - zt - - kr - - st - - sh + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 5 + replacement: + - hs + - zt + - kr + - st + - sh # A mess of broken Japanese, spoken by Felinds and Oni - type: language id: Nekomimetic - obfuscateSyllables: true - replacement: - - neko - - nyan - - mimi - - moe - - mofu - - fuwa - - kyaa - - kawaii - - poka - - munya - - puni - - munyu - - ufufu - - icha - - doki - - kyun - - kusu - - nya - - nyaa - - desu - - kis - - ama - - chuu - - baka - - hewo - - boop - - gato - - kit - - sune - - yori - - sou - - baka - - chan - - san - - kun - - mahou - - yatta - - suki - - usagi - - domo - - ori - - uwa - - zaazaa - - shiku - - puru - - ira - - heto - - etto + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 # May be too long even, we'll see. + replacement: + - neko + - nyan + - mimi + - moe + - mofu + - fuwa + - kyaa + - kawaii + - poka + - munya + - puni + - munyu + - ufufu + - icha + - doki + - kyun + - kusu + - nya + - nyaa + - desu + - kis + - ama + - chuu + - baka + - hewo + - boop + - gato + - kit + - sune + - yori + - sou + - baka + - chan + - san + - kun + - mahou + - yatta + - suki + - usagi + - domo + - ori + - uwa + - zaazaa + - shiku + - puru + - ira + - heto + - etto # Spoken by the Lizard race. - type: language id: Draconic - obfuscateSyllables: true - replacement: - - za - - az - - ze - - ez - - zi - - iz - - zo - - oz - - zu - - uz - - zs - - sz - - ha - - ah - - he - - eh - - hi - - ih - - ho - - oh - - hu - - uh - - hs - - sh - - la - - al - - le - - el - - li - - il - - lo - - ol - - lu - - ul - - ls - - sl - - ka - - ak - - ke - - ek - - ki - - ik - - ko - - ok - - ku - - uk - - ks - - sk - - sa - - as - - se - - es - - si - - is - - so - - os - - su - - us - - ss - - ss - - ra - - ar - - re - - er - - ri - - ir - - ro - - or - - ru - - ur - - rs - - sr - - a - - a - - e - - e - - i - - i - - o - - o - - u - - u - - s - - s + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - za + - az + - ze + - ez + - zi + - iz + - zo + - oz + - zu + - uz + - zs + - sz + - ha + - ah + - he + - eh + - hi + - ih + - ho + - oh + - hu + - uh + - hs + - sh + - la + - al + - le + - el + - li + - il + - lo + - ol + - lu + - ul + - ls + - sl + - ka + - ak + - ke + - ek + - ki + - ik + - ko + - ok + - ku + - uk + - ks + - sk + - sa + - as + - se + - es + - si + - is + - so + - os + - su + - us + - ss + - ss + - ra + - ar + - re + - er + - ri + - ir + - ro + - or + - ru + - ur + - rs + - sr + - a + - a + - e + - e + - i + - i + - o + - o + - u + - u + - s + - s # Spoken by the Vulpkanin race. - type: language id: Canilunzt - obfuscateSyllables: true - replacement: - - rur - - ya - - cen - - rawr - - bar - - kuk - - tek - - qat - - uk - - wu - - vuh - - tah - - tch - - schz - - auch - - ist - - ein - - entch - - zwichs - - tut - - mir - - wo - - bis - - es - - vor - - nic - - gro - - lll - - enem - - zandt - - tzch - - noch - - hel - - ischt - - far - - wa - - baram - - iereng - - tech - - lach - - sam - - mak - - lich - - gen - - or - - ag - - eck - - gec - - stag - - onn - - bin - - ket - - jarl - - vulf - - einech - - cresthz - - azunein - - ghzth + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - rur + - ya + - cen + - rawr + - bar + - kuk + - tek + - qat + - uk + - wu + - vuh + - tah + - tch + - schz + - auch + - ist + - ein + - entch + - zwichs + - tut + - mir + - wo + - bis + - es + - vor + - nic + - gro + # - lll + - enem + - zandt + - tzch + - noch + - hel + - ischt + - far + - wa + - baram + - iereng + - tech + - lach + - sam + - mak + - lich + - gen + - or + - ag + - eck + - gec + - stag + - onn + - bin + - ket + - jarl + - vulf + - einech + - cresthz + - azunein + - ghzth # The common language of the Sol system. - type: language id: SolCommon - obfuscateSyllables: true - replacement: - - tao - - shi - - tzu - - yi - - com - - be - - is - - i - - op - - vi - - ed - - lec - - mo - - cle - - te - - dis - - e + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - tao + - shi + - tzu + - yi + - com + - be + - is + - i + - op + - vi + - ed + - lec + - mo + - cle + - te + - dis + - e - type: language id: RobotTalk - obfuscateSyllables: true - replacement: - - 0 - - 1 - - 01 - - 10 - - 001 - - 100 - - 011 - - 110 - - 101 - - 010 + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 10 # Crazy + replacement: + - 0 + - 1 # Languages spoken by various critters. - type: language id: Cat - obfuscateSyllables: true - replacement: - - murr - - meow - - purr - - mrow + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - murr + - meow + - purr + - mrow - type: language id: Dog - obfuscateSyllables: true - replacement: - - woof - - bark - - ruff - - bork - - raff - - garr + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr - type: language id: Fox - obfuscateSyllables: true - replacement: - - bark - - gecker - - ruff - - raff - - garr + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - ruff + - raff + - garr + - yip + - yap + - myah - type: language id: Xeno - obfuscateSyllables: true - replacement: - - sss - - sSs - - SSS + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # I was crazy once + replacement: + - s + - S - type: language id: Monkey - obfuscateSyllables: true - replacement: - - ok - - ook - - oook - - ooook - - oooook + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # They locked me in a room... + replacement: + - o + - k - type: language id: Mouse - obfuscateSyllables: true - replacement: - - Squeak - - Piep - - Chuu - - Eeee - - Pip - - Fwiep - - Heep + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 3 + replacement: + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he - type: language id: Chicken - obfuscateSyllables: true - replacement: - - Coo - - Coot - - Cooot + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - co + - coo + - ot - type: language id: Duck - obfuscateSyllables: true - replacement: - - Quack - - Quack quack + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - qu + - ack + - quack - type: language id: Cow - obfuscateSyllables: true - replacement: - - Moo - - Mooo + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - moo + - mooo - type: language id: Sheep - obfuscateSyllables: true - replacement: - - Ba - - Baa - - Baaa + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - ba + - baa + - aa - type: language id: Kangaroo - obfuscateSyllables: true - replacement: - - Shreak - - Chuu + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - shre + - ack + - chuu + - choo - type: language id: Pig - obfuscateSyllables: true - replacement: - - Oink - - Oink oink + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - oink # Please someone come up with something better - type: language id: Crab - obfuscateSyllables: true - replacement: - - Click - - Click-clack - - Clack - - Tipi-tap - - Clik-tap - - Cliliick + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - click + - clack + - ti + - pi + - tap + - cli + - ick - type: language id: Kobold - obfuscateSyllables: true - replacement: - - Yip - - Grrar. - - Yap - - Bip - - Screet - - Gronk - - Hiss - - Eeee - - Yip + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml index ab3f6f3d1c1..e93117b1aad 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml @@ -35,7 +35,7 @@ - MobLayer - type: Stamina critThreshold: 115 - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - Nekomimetic diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index d23607b16d5..285c7340b21 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -65,7 +65,7 @@ Unsexed: MaleFelinid - type: Felinid - type: NoShoesSilentFootsteps - - type: LanguageSpeaker + - type: LanguageKnowledge speaks: - GalacticCommon - SolCommon From 726fd0f7aaeb1ee6ec21b536dbebe42dc2acb351 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 5 Jul 2024 23:50:34 +0000 Subject: [PATCH 19/21] Automatic Changelog Update (#459) --- Resources/Changelog/Changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 68ccf158b46..af45d9f81af 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -4351,3 +4351,13 @@ Entries: Have fun customizing your characters! id: 6144 time: '2024-07-05T22:53:36.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Tweak + message: Translator implants are now proper implants that can be removed. + - type: Tweak + message: Animalistic languages should now look less messy. + - type: Fix + message: Hopefully fixed language menu desync and other issues. + id: 6145 + time: '2024-07-05T23:49:47.0000000+00:00' From c8a9002efc6ac89c280e95900f2ab70d5d9e89cb Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Sat, 6 Jul 2024 03:19:35 +0300 Subject: [PATCH 20/21] Add Item Transfer System (#476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR adds the ability to transfer objects from one player's hand to another player's hand, as in SS13. I have little coding experience, so my solutions may not be ideal. --- # TODO - [x] Make the code work - [x] Add popup - [x] Write a summary of the code - [x] Сorrect inaccuracies

Media

https://youtu.be/zTQWTsYm1gw

--- # Changelog :cl: - add: Added system - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --------- Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> --- Content.Client/Input/ContentContexts.cs | 1 + .../OfferItem/OfferItemIndicatorsOverlay.cs | 72 ++++++++ Content.Client/OfferItem/OfferItemSystem.cs | 51 ++++++ .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 1 + Content.Client/Options/UI/Tabs/MiscTab.xaml | 1 + .../Options/UI/Tabs/MiscTab.xaml.cs | 5 + Content.Server/Alert/Click/AcceptingOffer.cs | 24 +++ Content.Server/OfferItem/OfferItemSystem.cs | 83 ++++++++++ Content.Shared/Alert/AlertType.cs | 3 +- Content.Shared/CCVar/CCVars.cs | 3 + Content.Shared/Input/ContentKeyFunctions.cs | 1 + .../OfferItem/OfferItemComponent.cs | 26 +++ .../SharedOfferItemSystem.Interactions.cs | 74 +++++++++ .../OfferItem/SharedOfferItemSystem.cs | 155 ++++++++++++++++++ Resources/Locale/en-US/alerts/alerts.ftl | 3 + .../en-US/escape-menu/ui/options-menu.ftl | 2 + .../en-US/interaction/offer-item-system.ftl | 13 ++ Resources/Prototypes/Alerts/alerts.yml | 10 ++ .../Prototypes/Entities/Mobs/Species/base.yml | 1 + .../Interface/Alerts/offer_item.rsi/meta.json | 14 ++ .../Alerts/offer_item.rsi/offer_item.png | Bin 0 -> 556 bytes .../Misc/give_item.rsi/give_item.png | Bin 0 -> 7276 bytes .../Interface/Misc/give_item.rsi/meta.json | 14 ++ Resources/keybinds.yml | 3 + 24 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs create mode 100644 Content.Client/OfferItem/OfferItemSystem.cs create mode 100644 Content.Server/Alert/Click/AcceptingOffer.cs create mode 100644 Content.Server/OfferItem/OfferItemSystem.cs create mode 100644 Content.Shared/OfferItem/OfferItemComponent.cs create mode 100644 Content.Shared/OfferItem/SharedOfferItemSystem.Interactions.cs create mode 100644 Content.Shared/OfferItem/SharedOfferItemSystem.cs create mode 100644 Resources/Locale/en-US/interaction/offer-item-system.ftl create mode 100644 Resources/Textures/Interface/Alerts/offer_item.rsi/meta.json create mode 100644 Resources/Textures/Interface/Alerts/offer_item.rsi/offer_item.png create mode 100644 Resources/Textures/Interface/Misc/give_item.rsi/give_item.png create mode 100644 Resources/Textures/Interface/Misc/give_item.rsi/meta.json diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index fa631938100..ca22ab095d6 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -68,6 +68,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.SmartEquipBelt); human.AddFunction(ContentKeyFunctions.OpenBackpack); human.AddFunction(ContentKeyFunctions.OpenBelt); + human.AddFunction(ContentKeyFunctions.OfferItem); human.AddFunction(ContentKeyFunctions.MouseMiddle); human.AddFunction(ContentKeyFunctions.ArcadeUp); human.AddFunction(ContentKeyFunctions.ArcadeDown); diff --git a/Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs b/Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs new file mode 100644 index 00000000000..16a314a2cf4 --- /dev/null +++ b/Content.Client/OfferItem/OfferItemIndicatorsOverlay.cs @@ -0,0 +1,72 @@ +using System.Numerics; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.UserInterface; +using Robust.Shared.Enums; +using Robust.Shared.Utility; + +namespace Content.Client.OfferItem; + +public sealed class OfferItemIndicatorsOverlay : Overlay +{ + private readonly IInputManager _inputManager; + private readonly IEntityManager _entMan; + private readonly IEyeManager _eye; + private readonly OfferItemSystem _offer; + + private readonly Texture _sight; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + private readonly Color _mainColor = Color.White.WithAlpha(0.3f); + private readonly Color _strokeColor = Color.Black.WithAlpha(0.5f); + private readonly float _scale = 0.6f; // 1 is a little big + + public OfferItemIndicatorsOverlay(IInputManager input, IEntityManager entMan, + IEyeManager eye, OfferItemSystem offerSys) + { + _inputManager = input; + _entMan = entMan; + _eye = eye; + _offer = offerSys; + + var spriteSys = _entMan.EntitySysManager.GetEntitySystem(); + _sight = spriteSys.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/give_item.rsi"), + "give_item")); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_offer.IsInOfferMode()) + return false; + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var mouseScreenPosition = _inputManager.MouseScreenPosition; + var mousePosMap = _eye.PixelToMap(mouseScreenPosition); + if (mousePosMap.MapId != args.MapId) + return; + + + var mousePos = mouseScreenPosition.Position; + var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f; + var limitedScale = uiScale > 1.25f ? 1.25f : uiScale; + + DrawSight(_sight, args.ScreenHandle, mousePos, limitedScale * _scale); + } + + private void DrawSight(Texture sight, DrawingHandleScreen screen, Vector2 centerPos, float scale) + { + var sightSize = sight.Size * scale; + var expandedSize = sightSize + new Vector2(7f, 7f); + + screen.DrawTextureRect(sight, + UIBox2.FromDimensions(centerPos - sightSize * 0.5f, sightSize), _strokeColor); + screen.DrawTextureRect(sight, + UIBox2.FromDimensions(centerPos - expandedSize * 0.5f, expandedSize), _mainColor); + } +} diff --git a/Content.Client/OfferItem/OfferItemSystem.cs b/Content.Client/OfferItem/OfferItemSystem.cs new file mode 100644 index 00000000000..51b8dcbc0bc --- /dev/null +++ b/Content.Client/OfferItem/OfferItemSystem.cs @@ -0,0 +1,51 @@ +using Content.Shared.CCVar; +using Content.Shared.OfferItem; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Shared.Configuration; + +namespace Content.Client.OfferItem; + +public sealed class OfferItemSystem : SharedOfferItemSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IEyeManager _eye = default!; + + public override void Initialize() + { + Subs.CVar(_cfg, CCVars.OfferModeIndicatorsPointShow, OnShowOfferIndicatorsChanged, true); + } + public override void Shutdown() + { + _overlayManager.RemoveOverlay(); + + base.Shutdown(); + } + + public bool IsInOfferMode() + { + var entity = _playerManager.LocalEntity; + + if (entity == null) + return false; + + return IsInOfferMode(entity.Value); + } + private void OnShowOfferIndicatorsChanged(bool isShow) + { + if (isShow) + { + _overlayManager.AddOverlay(new OfferItemIndicatorsOverlay( + _inputManager, + EntityManager, + _eye, + this)); + } + else + _overlayManager.RemoveOverlay(); + } +} diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 49e8099e0fb..9daca74dd3a 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -183,6 +183,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action +