using System.Numerics;
+using Content.Shared.Body.Components;
using Content.Shared.CardboardBox;
using Content.Shared.CardboardBox.Components;
using Content.Shared.Examine;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
+ private EntityQuery<BodyComponent> _bodyQuery;
+
public override void Initialize()
{
base.Initialize();
+
+ _bodyQuery = GetEntityQuery<BodyComponent>();
+
SubscribeNetworkEvent<PlayBoxEffectMessage>(OnBoxEffect);
}
if (!_examine.InRangeUnOccluded(sourcePos, mapPos, box.Distance, null))
continue;
+ // no effect for anything too exotic
+ if (!_bodyQuery.HasComp(mob))
+ continue;
+
var ent = Spawn(box.Effect, mapPos);
if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))
--- /dev/null
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Polymorph.Components;
+using Content.Shared.Polymorph.Systems;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Polymorph.Systems;
+
+public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ private EntityQuery<AppearanceComponent> _appearanceQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _appearanceQuery = GetEntityQuery<AppearanceComponent>();
+
+ SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
+ }
+
+ private void OnHandleState(Entity<ChameleonDisguiseComponent> ent, ref AfterAutoHandleStateEvent args)
+ {
+ CopyComp<SpriteComponent>(ent);
+ CopyComp<GenericVisualizerComponent>(ent);
+ CopyComp<SolutionContainerVisualsComponent>(ent);
+
+ // reload appearance to hopefully prevent any invisible layers
+ if (_appearanceQuery.TryComp(ent, out var appearance))
+ _appearance.QueueUpdate(ent, appearance);
+ }
+}
--- /dev/null
+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<ChameleonDisguiseComponent, GotEquippedHandEvent>(OnEquippedHand);
+ SubscribeLocalEvent<ChameleonDisguiseComponent, DisguiseToggleNoRotEvent>(OnToggleNoRot);
+ SubscribeLocalEvent<ChameleonDisguiseComponent, DisguiseToggleAnchoredEvent>(OnToggleAnchored);
+ }
+
+ private void OnEquippedHand(Entity<ChameleonDisguiseComponent> ent, ref GotEquippedHandEvent args)
+ {
+ if (!TryComp<PolymorphedEntityComponent>(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<ChameleonDisguiseComponent>(disguise);
+ comp.SourceEntity = entity;
+ comp.SourceProto = Prototype(entity)?.ID;
+ Dirty(disguise, comp);
+
+ // no sechud trolling
+ RemComp<StatusIconComponent>(disguise);
+
+ _appearance.CopyData(entity, disguise);
+
+ var mass = CompOrNull<PhysicsComponent>(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<MobThresholdsComponent>(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<ChameleonDisguiseComponent> ent, ref DisguiseToggleNoRotEvent args)
+ {
+ var xform = Transform(ent);
+ xform.NoLocalRotation = !xform.NoLocalRotation;
+ }
+
+ private void OnToggleAnchored(Entity<ChameleonDisguiseComponent> ent, ref DisguiseToggleAnchoredEvent args)
+ {
+ var uid = ent.Owner;
+ var xform = Transform(uid);
+ if (xform.Anchored)
+ _xform.Unanchor(uid, xform);
+ else
+ _xform.AnchorEntity((uid, xform));
+ }
+}
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Polymorph.Components;
+
+/// <summary>
+/// Component added to disguise entities.
+/// Used by client to copy over appearance from the disguise's source entity.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+public sealed partial class ChameleonDisguiseComponent : Component
+{
+ /// <summary>
+ /// The disguise source entity for copying the sprite.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public EntityUid SourceEntity;
+
+ /// <summary>
+ /// The source entity's prototype.
+ /// Used as a fallback if the source entity was deleted.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public EntProtoId? SourceProto;
+}
--- /dev/null
+using Content.Shared.Polymorph;
+using Content.Shared.Polymorph.Systems;
+using Content.Shared.Whitelist;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Polymorph.Components;
+
+/// <summary>
+/// A chameleon projector polymorphs you into a clicked entity, then polymorphs back when clicked on or destroyed.
+/// This creates a new dummy polymorph entity and copies the appearance over.
+/// </summary>
+[RegisterComponent, Access(typeof(SharedChameleonProjectorSystem))]
+public sealed partial class ChameleonProjectorComponent : Component
+{
+ /// <summary>
+ /// If non-null, whitelist for valid entities to disguise as.
+ /// </summary>
+ [DataField(required: true)]
+ public EntityWhitelist? Whitelist;
+
+ /// <summary>
+ /// If non-null, blacklist that prevents entities from being used even if they are in the whitelist.
+ /// </summary>
+ [DataField(required: true)]
+ public EntityWhitelist? Blacklist;
+
+ /// <summary>
+ /// Polymorph configuration for the disguise entity.
+ /// </summary>
+ [DataField(required: true)]
+ public PolymorphConfiguration Polymorph = new();
+
+ /// <summary>
+ /// Action for disabling your disguise's rotation.
+ /// </summary>
+ [DataField]
+ public EntProtoId NoRotAction = "ActionDisguiseNoRot";
+
+ /// <summary>
+ /// Action for anchoring your disguise in place.
+ /// </summary>
+ [DataField]
+ public EntProtoId AnchorAction = "ActionDisguiseAnchor";
+
+ /// <summary>
+ /// Minimum health to give the disguise.
+ /// </summary>
+ [DataField]
+ public float MinHealth = 1f;
+
+ /// <summary>
+ /// Maximum health to give the disguise, health scales with mass.
+ /// </summary>
+ [DataField]
+ public float MaxHealth = 100f;
+
+ /// <summary>
+ /// Popup shown to the user when they try to disguise as an invalid entity.
+ /// </summary>
+ [DataField]
+ public LocId InvalidPopup = "chameleon-projector-invalid";
+
+ /// <summary>
+ /// Popup shown to the user when they disguise as a valid entity.
+ /// </summary>
+ [DataField]
+ public LocId SuccessPopup = "chameleon-projector-success";
+}
--- /dev/null
+using Content.Shared.Actions;
+using Content.Shared.Interaction;
+using Content.Shared.Polymorph;
+using Content.Shared.Polymorph.Components;
+using Content.Shared.Popups;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Prototypes;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Shared.Polymorph.Systems;
+
+/// <summary>
+/// Handles whitelist/blacklist checking.
+/// Actual polymorphing and deactivation is done serverside.
+/// </summary>
+public abstract class SharedChameleonProjectorSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly ISerializationManager _serMan = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<ChameleonProjectorComponent, AfterInteractEvent>(OnInteract);
+ }
+
+ private void OnInteract(Entity<ChameleonProjectorComponent> ent, ref AfterInteractEvent args)
+ {
+ if (!args.CanReach || args.Target is not {} target)
+ return;
+
+ var user = args.User;
+ args.Handled = true;
+
+ if (IsInvalid(ent.Comp, target))
+ {
+ _popup.PopupClient(Loc.GetString(ent.Comp.InvalidPopup), target, user);
+ return;
+ }
+
+ _popup.PopupClient(Loc.GetString(ent.Comp.SuccessPopup), target, user);
+ Disguise(ent.Comp, user, target);
+ }
+
+ /// <summary>
+ /// Returns true if an entity cannot be used as a disguise.
+ /// </summary>
+ public bool IsInvalid(ChameleonProjectorComponent comp, EntityUid target)
+ {
+ return (comp.Whitelist?.IsValid(target, EntityManager) == false)
+ || (comp.Blacklist?.IsValid(target, EntityManager) == true);
+ }
+
+ /// <summary>
+ /// On server, polymorphs the user into an entity and sets up the disguise.
+ /// </summary>
+ public virtual void Disguise(ChameleonProjectorComponent comp, EntityUid user, EntityUid entity)
+ {
+ }
+
+ /// <summary>
+ /// Copy a component from the source entity/prototype to the disguise entity.
+ /// </summary>
+ /// <remarks>
+ /// This would probably be a good thing to add to engine in the future.
+ /// </remarks>
+ protected bool CopyComp<T>(Entity<ChameleonDisguiseComponent> ent) where T: Component, new()
+ {
+ if (!GetSrcComp<T>(ent.Comp, out var src))
+ return true;
+
+ // remove then re-add to prevent a funny
+ RemComp<T>(ent);
+ var dest = AddComp<T>(ent);
+ _serMan.CopyTo(src, ref dest, notNullableOverride: true);
+ Dirty(ent, dest);
+ return false;
+ }
+
+ /// <summary>
+ /// Try to get a single component from the source entity/prototype.
+ /// </summary>
+ private bool GetSrcComp<T>(ChameleonDisguiseComponent comp, [NotNullWhen(true)] out T? src) where T: Component
+ {
+ src = null;
+ if (TryComp(comp.SourceEntity, out src))
+ return true;
+
+ if (comp.SourceProto is not {} protoId)
+ return false;
+
+ if (!_proto.TryIndex<EntityPrototype>(protoId, out var proto))
+ return false;
+
+ return proto.TryGetComponent(out src);
+ }
+}
+
+/// <summary>
+/// Action event for toggling transform NoRot on a disguise.
+/// </summary>
+public sealed partial class DisguiseToggleNoRotEvent : InstantActionEvent
+{
+}
+
+/// <summary>
+/// Action event for toggling transform Anchored on a disguise.
+/// </summary>
+public sealed partial class DisguiseToggleAnchoredEvent : InstantActionEvent
+{
+}
--- /dev/null
+chameleon-projector-invalid = You can't disguise as that!
+chameleon-projector-success = Projected new disguise.
uplink-mobcat-microbomb-name = SyndiCat
uplink-mobcat-microbomb-desc = A hand cat equipped with a microbomb implant. Explodes when seriously injured. Can bite painfully
+uplink-chameleon-projector-name = Chameleon Projector
+uplink-chameleon-projector-desc = Disappear in plain sight by creating a hologram of an item around you. Do not use this to play the game "Object Search".
+
# Pointless
uplink-revolver-cap-gun-name = Cap Gun
uplink-revolver-cap-gun-desc = Looks almost like the real thing! Ages 8 and up.
categories:
- UplinkDeception
+- type: listing
+ id: UplinkChameleonProjector
+ name: uplink-chameleon-projector-name
+ description: uplink-chameleon-projector-desc
+ productEntity: ChameleonProjector
+ cost:
+ Telecrystal: 7
+ categories:
+ - UplinkDeception
+
- type: listing
id: UplinkHeadsetEncryptionKey
name: uplink-encryption-key-name
--- /dev/null
+- type: entity
+ parent: BaseItem
+ id: ChameleonProjector
+ name: chameleon projector
+ description: Holoparasite technology used to create a hard-light replica of any object around you. Disguise is destroyed when picked up or deactivated.
+ components:
+ - type: Sprite
+ sprite: /Textures/Objects/Devices/chameleon_projector.rsi
+ state: icon
+ - type: ChameleonProjector
+ whitelist:
+ components:
+ - Anchorable
+ - Item
+ 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
+
+- type: entity
+ noSpawn: true
+ parent: BaseMob
+ id: ChameleonDisguise
+ name: Urist McKleiner
+ components:
+ # this and the name/desc get replaced, this is just placeholder incase something goes wrong
+ - 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: 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
+- type: entity
+ noSpawn: true
+ id: ActionDisguiseNoRot
+ name: Toggle Rotation
+ description: Use this to prevent your disguise from rotating, making it easier to hide in some scenarios.
+ components:
+ - type: InstantAction
+ icon: Interface/VerbIcons/refresh.svg.192dpi.png
+ event: !type:DisguiseToggleNoRotEvent
+
+- type: entity
+ noSpawn: true
+ id: ActionDisguiseAnchor
+ name: Toggle Anchored
+ description: For many objects you will want to be anchored to not be completely obvious.
+ components:
+ - type: InstantAction
+ icon:
+ sprite: Objects/Tools/wrench.rsi
+ state: icon
+ event: !type:DisguiseToggleAnchoredEvent
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from tgstation at ",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon",
+ "delays": [
+ [
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2,
+ 0.2
+ ]
+ ]
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}