From 80e148c265dc8602f50e3941dbd94bb396a1f5b5 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:15:41 +0000 Subject: [PATCH] cham projector fixes/rewrite (#27111) * cant disguise to thing in a container * copy cigarette visualiser * prevent aghost throwing an error * make disguises die in space * fuck it rewrite it to not use polymorph * fix action troll * oop * add vebr * add access to the components * 2/3 * fix * relay damage from disguise to user * fix integrity * :trollface: * :trollface: * m * kill integrity * fix a bug * review * remove them from component * relay flash effect to the disguise * fix icon being weird * change method since multiple systems cant handle same network event * :trollface: * actually network Disguise real --------- Co-authored-by: deltanedas <@deltanedas:kde.org> --- .../Effects/ColorFlashEffectSystem.cs | 10 + .../Systems/ChameleonProjectorSystem.cs | 30 +++ .../Systems/ChameleonProjectorSystem.cs | 96 +------- .../Components/ChameleonDisguiseComponent.cs | 16 +- .../Components/ChameleonDisguisedComponent.cs | 25 ++ .../Components/ChameleonProjectorComponent.cs | 19 +- .../Systems/SharedChameleonProjectorSystem.cs | 229 ++++++++++++++++-- .../chameleon-projector.ftl | 2 + .../Objects/Devices/chameleon_projector.yml | 24 +- 9 files changed, 311 insertions(+), 140 deletions(-) create mode 100644 Content.Shared/Polymorph/Components/ChameleonDisguisedComponent.cs diff --git a/Content.Client/Effects/ColorFlashEffectSystem.cs b/Content.Client/Effects/ColorFlashEffectSystem.cs index 956c946524..b584aa9ad1 100644 --- a/Content.Client/Effects/ColorFlashEffectSystem.cs +++ b/Content.Client/Effects/ColorFlashEffectSystem.cs @@ -124,6 +124,10 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem continue; } + var targetEv = new GetFlashEffectTargetEvent(ent); + RaiseLocalEvent(ent, ref targetEv); + ent = targetEv.Target; + EnsureComp(ent, out comp); comp.NetSyncEnabled = false; comp.Color = sprite.Color; @@ -132,3 +136,9 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem } } } + +/// +/// Raised on an entity to change the target for a color flash effect. +/// +[ByRefEvent] +public record struct GetFlashEffectTargetEvent(EntityUid Target); diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs index 5ba4878c6d..8ba09c6617 100644 --- a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs +++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -1,7 +1,10 @@ +using Content.Client.Effects; +using Content.Client.Smoking; using Content.Shared.Chemistry.Components; using Content.Shared.Polymorph.Components; using Content.Shared.Polymorph.Systems; using Robust.Client.GameObjects; +using Robust.Shared.Player; namespace Content.Client.Polymorph.Systems; @@ -10,14 +13,20 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private EntityQuery _appearanceQuery; + private EntityQuery _spriteQuery; public override void Initialize() { base.Initialize(); _appearanceQuery = GetEntityQuery(); + _spriteQuery = GetEntityQuery(); SubscribeLocalEvent(OnHandleState); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetFlashEffectTargetEvent); } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -25,9 +34,30 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem CopyComp(ent); CopyComp(ent); CopyComp(ent); + CopyComp(ent); // reload appearance to hopefully prevent any invisible layers if (_appearanceQuery.TryComp(ent, out var appearance)) _appearance.QueueUpdate(ent, appearance); } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + if (!_spriteQuery.TryComp(ent, out var sprite)) + return; + + ent.Comp.WasVisible = sprite.Visible; + sprite.Visible = false; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (_spriteQuery.TryComp(ent, out var sprite)) + sprite.Visible = ent.Comp.WasVisible; + } + + private void OnGetFlashEffectTargetEvent(Entity ent, ref GetFlashEffectTargetEvent args) + { + args.Target = ent.Comp.Disguise; + } } diff --git a/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs index 1586973a21..ab12f2764c 100644 --- a/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs +++ b/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -1,99 +1,5 @@ -using Content.Server.Polymorph.Components; -using Content.Shared.Actions; -using Content.Shared.Construction.Components; -using Content.Shared.Hands; -using Content.Shared.Mobs; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Polymorph; -using Content.Shared.Polymorph.Components; using Content.Shared.Polymorph.Systems; -using Content.Shared.StatusIcon.Components; -using Robust.Shared.Physics.Components; namespace Content.Server.Polymorph.Systems; -public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem -{ - [Dependency] private readonly MetaDataSystem _meta = default!; - [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; - [Dependency] private readonly PolymorphSystem _polymorph = default!; - [Dependency] private readonly SharedActionsSystem _actions = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedTransformSystem _xform = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnEquippedHand); - SubscribeLocalEvent(OnToggleNoRot); - SubscribeLocalEvent(OnToggleAnchored); - } - - private void OnEquippedHand(Entity ent, ref GotEquippedHandEvent args) - { - if (!TryComp(ent, out var poly)) - return; - - _polymorph.Revert((ent, poly)); - args.Handled = true; - } - - public override void Disguise(ChameleonProjectorComponent proj, EntityUid user, EntityUid entity) - { - if (_polymorph.PolymorphEntity(user, proj.Polymorph) is not {} disguise) - return; - - // make disguise look real (for simple things at least) - var meta = MetaData(entity); - _meta.SetEntityName(disguise, meta.EntityName); - _meta.SetEntityDescription(disguise, meta.EntityDescription); - - var comp = EnsureComp(disguise); - comp.SourceEntity = entity; - comp.SourceProto = Prototype(entity)?.ID; - Dirty(disguise, comp); - - // no sechud trolling - RemComp(disguise); - - _appearance.CopyData(entity, disguise); - - var mass = CompOrNull(entity)?.Mass ?? 0f; - - // let the disguise die when its taken enough damage, which then transfers to the player - // health is proportional to mass, and capped to not be insane - if (TryComp(disguise, out var thresholds)) - { - // if the player is of flesh and blood, cap max health to theirs - // so that when reverting damage scales 1:1 and not round removing - var playerMax = _mobThreshold.GetThresholdForState(user, MobState.Dead).Float(); - var max = playerMax == 0f ? proj.MaxHealth : Math.Max(proj.MaxHealth, playerMax); - - var health = Math.Clamp(mass, proj.MinHealth, proj.MaxHealth); - _mobThreshold.SetMobStateThreshold(disguise, health, MobState.Critical, thresholds); - _mobThreshold.SetMobStateThreshold(disguise, max, MobState.Dead, thresholds); - } - - // add actions for controlling transform aspects - _actions.AddAction(disguise, proj.NoRotAction); - _actions.AddAction(disguise, proj.AnchorAction); - } - - private void OnToggleNoRot(Entity ent, ref DisguiseToggleNoRotEvent args) - { - var xform = Transform(ent); - xform.NoLocalRotation = !xform.NoLocalRotation; - } - - private void OnToggleAnchored(Entity ent, ref DisguiseToggleAnchoredEvent args) - { - var uid = ent.Owner; - var xform = Transform(uid); - if (xform.Anchored) - _xform.Unanchor(uid, xform); - else - _xform.AnchorEntity((uid, xform)); - } -} +public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem; diff --git a/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs b/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs index 2b9fba7b39..282106b8f6 100644 --- a/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs +++ b/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Polymorph.Systems; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -7,9 +8,22 @@ namespace Content.Shared.Polymorph.Components; /// Component added to disguise entities. /// Used by client to copy over appearance from the disguise's source entity. /// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[RegisterComponent, NetworkedComponent, Access(typeof(SharedChameleonProjectorSystem))] +[AutoGenerateComponentState(true)] public sealed partial class ChameleonDisguiseComponent : Component { + /// + /// The user of this disguise. + /// + [DataField] + public EntityUid User; + + /// + /// The projector that created this disguise. + /// + [DataField] + public EntityUid Projector; + /// /// The disguise source entity for copying the sprite. /// diff --git a/Content.Shared/Polymorph/Components/ChameleonDisguisedComponent.cs b/Content.Shared/Polymorph/Components/ChameleonDisguisedComponent.cs new file mode 100644 index 0000000000..cd2e26c420 --- /dev/null +++ b/Content.Shared/Polymorph/Components/ChameleonDisguisedComponent.cs @@ -0,0 +1,25 @@ +using Content.Shared.Polymorph.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Polymorph.Components; + +/// +/// Added to a player when they use a chameleon projector. +/// Handles making them invisible and revealing when damaged enough or switching hands. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedChameleonProjectorSystem))] +[AutoGenerateComponentState] +public sealed partial class ChameleonDisguisedComponent : Component +{ + /// + /// The disguise entity parented to the player. + /// + [DataField, AutoNetworkedField] + public EntityUid Disguise; + + /// + /// For client, whether the user's sprite was previously visible or not. + /// + [DataField] + public bool WasVisible; +} diff --git a/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs b/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs index 239b5236f2..1b289c54fc 100644 --- a/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs +++ b/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs @@ -1,4 +1,3 @@ -using Content.Shared.Polymorph; using Content.Shared.Polymorph.Systems; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; @@ -25,22 +24,26 @@ public sealed partial class ChameleonProjectorComponent : Component public EntityWhitelist? Blacklist; /// - /// Polymorph configuration for the disguise entity. + /// Disguise entity to spawn and use. /// [DataField(required: true)] - public PolymorphConfiguration Polymorph = new(); + public EntProtoId DisguiseProto = string.Empty; /// /// Action for disabling your disguise's rotation. /// [DataField] public EntProtoId NoRotAction = "ActionDisguiseNoRot"; + [DataField] + public EntityUid? NoRotActionEntity; /// /// Action for anchoring your disguise in place. /// [DataField] public EntProtoId AnchorAction = "ActionDisguiseAnchor"; + [DataField] + public EntityUid? AnchorActionEntity; /// /// Minimum health to give the disguise. @@ -55,14 +58,8 @@ public sealed partial class ChameleonProjectorComponent : Component public float MaxHealth = 100f; /// - /// Popup shown to the user when they try to disguise as an invalid entity. + /// User currently disguised by this projector, if any /// [DataField] - public LocId InvalidPopup = "chameleon-projector-invalid"; - - /// - /// Popup shown to the user when they disguise as a valid entity. - /// - [DataField] - public LocId SuccessPopup = "chameleon-projector-success"; + public EntityUid? Disguised; } diff --git a/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs index 00096b7d40..99737996b0 100644 --- a/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs +++ b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs @@ -1,67 +1,264 @@ using Content.Shared.Actions; +using Content.Shared.Coordinates; +using Content.Shared.Damage; +using Content.Shared.Hands; using Content.Shared.Interaction; -using Content.Shared.Polymorph; +using Content.Shared.Item; using Content.Shared.Polymorph.Components; using Content.Shared.Popups; -using Robust.Shared.Serialization.Manager; -using Robust.Shared.Prototypes; -using System.Diagnostics.CodeAnalysis; +using Content.Shared.Storage.Components; +using Content.Shared.Verbs; using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Polymorph.Systems; /// -/// Handles whitelist/blacklist checking. -/// Actual polymorphing and deactivation is done serverside. +/// Handles disguise validation, disguising and revealing. +/// Most appearance copying is done clientside. /// public abstract class SharedChameleonProjectorSystem : EntitySystem { + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly ISerializationManager _serMan = default!; + [Dependency] private readonly MetaDataSystem _meta = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnDisguiseInteractHand, before: [typeof(SharedItemSystem)]); + SubscribeLocalEvent(OnDisguiseDamaged); + SubscribeLocalEvent(OnDisguiseInsertAttempt); + SubscribeLocalEvent(OnDisguiseShutdown); + SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent>(OnGetVerbs); + SubscribeLocalEvent(OnToggleNoRot); + SubscribeLocalEvent(OnToggleAnchored); + SubscribeLocalEvent(OnDeselected); + SubscribeLocalEvent(OnUnequipped); + SubscribeLocalEvent(OnProjectorShutdown); } + #region Disguise entity + + private void OnDisguiseInteractHand(Entity ent, ref InteractHandEvent args) + { + TryReveal(ent.Comp.User); + args.Handled = true; + } + + private void OnDisguiseDamaged(Entity ent, ref DamageChangedEvent args) + { + // this mirrors damage 1:1 + if (args.DamageDelta is {} damage) + _damageable.TryChangeDamage(ent.Comp.User, damage); + } + + private void OnDisguiseInsertAttempt(Entity ent, ref InsertIntoEntityStorageAttemptEvent args) + { + // stay parented to the user, not the storage + args.Cancelled = true; + } + + private void OnDisguiseShutdown(Entity ent, ref ComponentShutdown args) + { + _actions.RemoveProvidedActions(ent.Comp.User, ent.Comp.Projector); + } + + #endregion + + #region Projector + private void OnInteract(Entity ent, ref AfterInteractEvent args) { - if (!args.CanReach || args.Target is not {} target) + if (args.Handled || !args.CanReach || args.Target is not {} target) + return; + + args.Handled = true; + TryDisguise(ent, args.User, target); + } + + private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess) return; var user = args.User; - args.Handled = true; + var target = args.Target; + args.Verbs.Add(new UtilityVerb() + { + Act = () => + { + TryDisguise(ent, user, target); + }, + Text = Loc.GetString("chameleon-projector-set-disguise") + }); + } + + public bool TryDisguise(Entity ent, EntityUid user, EntityUid target) + { + if (_container.IsEntityInContainer(target)) + { + _popup.PopupClient(Loc.GetString("chameleon-projector-inside-container"), target, user); + return false; + } if (IsInvalid(ent.Comp, target)) { - _popup.PopupClient(Loc.GetString(ent.Comp.InvalidPopup), target, user); - return; + _popup.PopupClient(Loc.GetString("chameleon-projector-invalid"), target, user); + return false; } - _popup.PopupClient(Loc.GetString(ent.Comp.SuccessPopup), target, user); - Disguise(ent.Comp, user, target); + _popup.PopupClient(Loc.GetString("chameleon-projector-success"), target, user); + Disguise(ent, user, target); + return true; } + private void OnToggleNoRot(Entity ent, ref DisguiseToggleNoRotEvent args) + { + if (ent.Comp.Disguised is not {} uid) + return; + + var xform = Transform(uid); + _xform.SetLocalRotationNoLerp(uid, 0, xform); + xform.NoLocalRotation = !xform.NoLocalRotation; + args.Handled = true; + } + + private void OnToggleAnchored(Entity ent, ref DisguiseToggleAnchoredEvent args) + { + if (ent.Comp.Disguised is not {} uid) + return; + + var xform = Transform(uid); + if (xform.Anchored) + _xform.Unanchor(uid, xform); + else + _xform.AnchorEntity((uid, xform)); + + args.Handled = true; + } + + private void OnDeselected(Entity ent, ref HandDeselectedEvent args) + { + RevealProjector(ent); + } + + private void OnUnequipped(Entity ent, ref GotUnequippedHandEvent args) + { + RevealProjector(ent); + } + + private void OnProjectorShutdown(Entity ent, ref ComponentShutdown args) + { + RevealProjector(ent); + } + + #endregion + + #region API + /// /// Returns true if an entity cannot be used as a disguise. /// public bool IsInvalid(ChameleonProjectorComponent comp, EntityUid target) { - return _whitelistSystem.IsWhitelistFail(comp.Whitelist, target) - || _whitelistSystem.IsBlacklistPass(comp.Blacklist, target); + return _whitelist.IsWhitelistFail(comp.Whitelist, target) + || _whitelist.IsBlacklistPass(comp.Blacklist, target); } /// /// On server, polymorphs the user into an entity and sets up the disguise. /// - public virtual void Disguise(ChameleonProjectorComponent comp, EntityUid user, EntityUid entity) + public void Disguise(Entity ent, EntityUid user, EntityUid entity) { + var proj = ent.Comp; + + // no spawning prediction sorry + if (_net.IsClient) + return; + + // reveal first to allow quick switching + TryReveal(user); + + // add actions for controlling transform aspects + _actions.AddAction(user, ref proj.NoRotActionEntity, proj.NoRotAction, container: ent); + _actions.AddAction(user, ref proj.AnchorActionEntity, proj.AnchorAction, container: ent); + + proj.Disguised = user; + + var disguise = SpawnAttachedTo(proj.DisguiseProto, user.ToCoordinates()); + + var disguised = AddComp(user); + disguised.Disguise = disguise; + Dirty(user, disguised); + + // make disguise look real (for simple things at least) + var meta = MetaData(entity); + _meta.SetEntityName(disguise, meta.EntityName); + _meta.SetEntityDescription(disguise, meta.EntityDescription); + + var comp = EnsureComp(disguise); + comp.User = user; + comp.Projector = ent; + comp.SourceEntity = entity; + comp.SourceProto = Prototype(entity)?.ID; + Dirty(disguise, comp); + + // item disguises can be picked up to be revealed, also makes sure their examine size is correct + CopyComp((disguise, comp)); + + _appearance.CopyData(entity, disguise); } + /// + /// Removes the disguise, if the user is disguised. + /// + public bool TryReveal(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + if (TryComp(ent.Comp.Disguise, out var disguise) + && TryComp(disguise.Projector, out var proj)) + { + proj.Disguised = null; + } + + var xform = Transform(ent); + xform.NoLocalRotation = false; + _xform.Unanchor(ent, xform); + + Del(ent.Comp.Disguise); + RemComp(ent); + return true; + } + + /// + /// Reveal a projector's user, if any. + /// + public void RevealProjector(Entity ent) + { + if (ent.Comp.Disguised is {} user) + TryReveal(user); + } + + #endregion + /// /// Copy a component from the source entity/prototype to the disguise entity. /// diff --git a/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl b/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl index 8a79516077..b525c9da1a 100644 --- a/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl +++ b/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl @@ -1,2 +1,4 @@ +chameleon-projector-inside-container = There's no room to scan that! chameleon-projector-invalid = You can't disguise as that! chameleon-projector-success = Projected new disguise. +chameleon-projector-set-disguise = Set Disguise diff --git a/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml index f07ae63569..b6819a18b9 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml @@ -17,15 +17,12 @@ blacklist: components: - ChameleonDisguise # no becoming kleiner - - InsideEntityStorage # no clark kent going in phone booth and becoming superman - MindContainer # no - Pda # PDAs currently make you invisible /!\ - polymorph: - entity: ChameleonDisguise + disguiseProto: ChameleonDisguise - type: entity categories: [ HideSpawnMenu ] - parent: BaseMob id: ChameleonDisguise name: Urist McKleiner components: @@ -33,20 +30,11 @@ - type: Sprite sprite: /Textures/Mobs/Species/Human/parts.rsi state: full - # so people can attempt to pick it up - - type: Item - # so it can take damage - # projector system sets health to be proportional to mass + - type: Transform + noRot: true # players rotation and anchor is used instead + - type: InteractionOutline + - type: Clickable - type: Damageable - - type: MobState - - type: MobThresholds - thresholds: - 0: Alive - 1: Critical - 200: Dead - - type: MovementSpeedModifier - baseWalkSpeed: 1 # precise movement for the perfect spot - baseSprintSpeed: 5 # the jig is up - type: ChameleonDisguise # actions @@ -57,6 +45,7 @@ components: - type: InstantAction icon: Interface/VerbIcons/refresh.svg.192dpi.png + itemIconStyle: BigAction event: !type:DisguiseToggleNoRotEvent - type: entity @@ -68,4 +57,5 @@ icon: sprite: Objects/Tools/wrench.rsi state: icon + itemIconStyle: BigAction event: !type:DisguiseToggleAnchoredEvent