diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs index 5ba4878c6d..4acbf540f0 100644 --- a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs +++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -1,3 +1,4 @@ +using Content.Client.Smoking; using Content.Shared.Chemistry.Components; using Content.Shared.Polymorph.Components; using Content.Shared.Polymorph.Systems; @@ -10,14 +11,19 @@ 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); } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -25,9 +31,25 @@ 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; + } } diff --git a/Content.Client/_White/Overlays/ThermalVisionOverlay.cs b/Content.Client/_White/Overlays/ThermalVisionOverlay.cs index 72b6619d1a..fcc4863ff9 100644 --- a/Content.Client/_White/Overlays/ThermalVisionOverlay.cs +++ b/Content.Client/_White/Overlays/ThermalVisionOverlay.cs @@ -77,7 +77,7 @@ public sealed class ThermalVisionOverlay : Overlay var entities = _entity.EntityQueryEnumerator(); while (entities.MoveNext(out var uid, out _, out var sprite, out var xform)) { - if (!CanSee(uid)) + if (!CanSee(uid, sprite)) continue; var entity = uid; @@ -114,7 +114,7 @@ public sealed class ThermalVisionOverlay : Overlay Angle eyeRot) { var (uid, sprite, xform) = ent; - if (xform.MapID != map || HasOccluders(uid) || !CanSee(uid)) + if (xform.MapID != map || HasOccluders(uid) || !CanSee(uid, sprite)) return; var position = _transform.GetWorldPosition(xform); @@ -123,9 +123,9 @@ public sealed class ThermalVisionOverlay : Overlay sprite.Render(handle, eyeRot, rotation, position: position); } - private bool CanSee(EntityUid ent) + private bool CanSee(EntityUid ent, SpriteComponent sprite) { - return !_entity.HasComponent(ent); + return sprite.Visible && !_entity.HasComponent(ent); } private bool HasOccluders(EntityUid ent) 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..0b0b18b254 --- /dev/null +++ b/Content.Shared/Polymorph/Components/ChameleonDisguisedComponent.cs @@ -0,0 +1,24 @@ +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))] +public sealed partial class ChameleonDisguisedComponent : Component +{ + /// + /// The disguise entity parented to the player. + /// + [DataField] + 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..3e2966af7e 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. @@ -54,6 +57,12 @@ public sealed partial class ChameleonProjectorComponent : Component [DataField] public float MaxHealth = 100f; + /// + /// Popup shown to the user when they try to disguise as an entity inside a container. + /// + [DataField] + public LocId ContainerPopup = "chameleon-projector-inside-container"; + /// /// Popup shown to the user when they try to disguise as an invalid entity. /// @@ -65,4 +74,10 @@ public sealed partial class ChameleonProjectorComponent : Component /// [DataField] public LocId SuccessPopup = "chameleon-projector-success"; + + /// + /// User currently disguised by this projector, if any + /// + [DataField] + public EntityUid? Disguised; } diff --git a/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs index c1abfc526f..9944c9ba32 100644 --- a/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs +++ b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs @@ -1,49 +1,173 @@ using Content.Shared.Actions; +using Content.Shared.Construction.Components; +using Content.Shared.Coordinates; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Hands; using Content.Shared.Interaction; +using Content.Shared.Item; using Content.Shared.Polymorph; using Content.Shared.Polymorph.Components; using Content.Shared.Popups; -using Robust.Shared.Serialization.Manager; +using Content.Shared.Verbs; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Physics.Components; 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 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 SharedTransformSystem _xform = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnDisguiseInteractHand, before: [typeof(SharedItemSystem)]); + SubscribeLocalEvent(OnDisguiseDamaged); + 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) + { + // anything that would damage both like an explosion gets doubled + // feature? projector makes your atoms weaker or some bs + if (args.DamageDelta is {} damage) + _damageable.TryChangeDamage(ent.Comp.User, damage); + } + + 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(ent.Comp.ContainerPopup), target, user); + return false; + } if (IsInvalid(ent.Comp, target)) { _popup.PopupClient(Loc.GetString(ent.Comp.InvalidPopup), target, user); - return; + return false; } _popup.PopupClient(Loc.GetString(ent.Comp.SuccessPopup), target, user); - Disguise(ent.Comp, user, target); + 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. /// @@ -56,10 +180,81 @@ public abstract class SharedChameleonProjectorSystem : EntitySystem /// /// 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 e021281021..c9d2dafad9 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml @@ -15,15 +15,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 noSpawn: true - parent: BaseMob id: ChameleonDisguise name: Urist McKleiner components: @@ -31,20 +28,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