]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
prop hunt ss14 (real) (#26691)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Wed, 17 Apr 2024 21:48:35 +0000 (21:48 +0000)
committerGitHub <noreply@github.com>
Wed, 17 Apr 2024 21:48:35 +0000 (14:48 -0700)
* texture appropriation

* add code for projector

* add chameleon projector yml

* damage and actions

* prevent small props being killed round removing you (700 damage from a single shot)

* tweak default

* oop

* do appearance properly, need engine update

* fix bugs, blacklist pda

* remove status icons

* amou

* sus

* fix test + make props fast

* amouuuung

* remove funny log

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
14 files changed:
Content.Client/CardboardBox/CardboardBoxSystem.cs
Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs [new file with mode: 0644]
Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs [new file with mode: 0644]
Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs [new file with mode: 0644]
Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs [new file with mode: 0644]
Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl [new file with mode: 0644]
Resources/Locale/en-US/store/uplink-catalog.ftl
Resources/Prototypes/Catalog/uplink_catalog.yml
Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml [new file with mode: 0644]
Resources/Textures/Objects/Devices/chameleon_projector.rsi/icon.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-left.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-right.png [new file with mode: 0644]
Resources/Textures/Objects/Devices/chameleon_projector.rsi/meta.json [new file with mode: 0644]

index 90a21d8e41b01d3c4f1b2ca04e3733113f9bde7a..925013db109d9f4dd2ae520bfdd2f55c766e1257 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Shared.Body.Components;
 using Content.Shared.CardboardBox;
 using Content.Shared.CardboardBox.Components;
 using Content.Shared.Examine;
@@ -13,9 +14,14 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
     [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);
     }
 
@@ -59,6 +65,10 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
             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))
diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs
new file mode 100644 (file)
index 0000000..5ba4878
--- /dev/null
@@ -0,0 +1,33 @@
+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);
+    }
+}
diff --git a/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs
new file mode 100644 (file)
index 0000000..1586973
--- /dev/null
@@ -0,0 +1,99 @@
+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));
+    }
+}
diff --git a/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs b/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs
new file mode 100644 (file)
index 0000000..2b9fba7
--- /dev/null
@@ -0,0 +1,25 @@
+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;
+}
diff --git a/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs b/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs
new file mode 100644 (file)
index 0000000..239b523
--- /dev/null
@@ -0,0 +1,68 @@
+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";
+}
diff --git a/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs
new file mode 100644 (file)
index 0000000..c1abfc5
--- /dev/null
@@ -0,0 +1,113 @@
+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
+{
+}
diff --git a/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl b/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl
new file mode 100644 (file)
index 0000000..8a79516
--- /dev/null
@@ -0,0 +1,2 @@
+chameleon-projector-invalid = You can't disguise as that!
+chameleon-projector-success = Projected new disguise.
index 8442725566e486d4261f060dca222769e71b3307..57a309ac69a914c96d53b2c2590534f0064be5f0 100644 (file)
@@ -367,6 +367,9 @@ uplink-slipocalypse-clustersoap-desc = Scatters arounds small pieces of syndicat
 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.
index b1e6b88435beae36262fe85f67a2465e2d32bafe..6375ac37f7fce57d24998431298987823e275111 100644 (file)
   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
diff --git a/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml
new file mode 100644 (file)
index 0000000..e021281
--- /dev/null
@@ -0,0 +1,71 @@
+- 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
diff --git a/Resources/Textures/Objects/Devices/chameleon_projector.rsi/icon.png b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/icon.png
new file mode 100644 (file)
index 0000000..ce20b5e
Binary files /dev/null and b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-left.png b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-left.png
new file mode 100644 (file)
index 0000000..2d38631
Binary files /dev/null and b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-right.png b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-right.png
new file mode 100644 (file)
index 0000000..1704b9c
Binary files /dev/null and b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Devices/chameleon_projector.rsi/meta.json b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/meta.json
new file mode 100644 (file)
index 0000000..3eb42e9
--- /dev/null
@@ -0,0 +1,32 @@
+{
+  "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
+    }
+  ]
+}