From 395c33024cb9d4e80078e3ab3c70c1e92e7fd5bc Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:48:35 +0000 Subject: [PATCH] prop hunt ss14 (real) (#26691) * 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> --- .../CardboardBox/CardboardBoxSystem.cs | 10 ++ .../Systems/ChameleonProjectorSystem.cs | 33 +++++ .../Systems/ChameleonProjectorSystem.cs | 99 +++++++++++++++ .../Components/ChameleonDisguiseComponent.cs | 25 ++++ .../Components/ChameleonProjectorComponent.cs | 68 +++++++++++ .../Systems/SharedChameleonProjectorSystem.cs | 113 ++++++++++++++++++ .../chameleon-projector.ftl | 2 + .../Locale/en-US/store/uplink-catalog.ftl | 3 + .../Prototypes/Catalog/uplink_catalog.yml | 10 ++ .../Objects/Devices/chameleon_projector.yml | 71 +++++++++++ .../Devices/chameleon_projector.rsi/icon.png | Bin 0 -> 716 bytes .../chameleon_projector.rsi/inhand-left.png | Bin 0 -> 306 bytes .../chameleon_projector.rsi/inhand-right.png | Bin 0 -> 316 bytes .../Devices/chameleon_projector.rsi/meta.json | 32 +++++ 14 files changed, 466 insertions(+) create mode 100644 Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs create mode 100644 Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs create mode 100644 Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs create mode 100644 Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs create mode 100644 Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs create mode 100644 Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml create mode 100644 Resources/Textures/Objects/Devices/chameleon_projector.rsi/icon.png create mode 100644 Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Devices/chameleon_projector.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Devices/chameleon_projector.rsi/meta.json diff --git a/Content.Client/CardboardBox/CardboardBoxSystem.cs b/Content.Client/CardboardBox/CardboardBoxSystem.cs index 90a21d8e41..925013db10 100644 --- a/Content.Client/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Client/CardboardBox/CardboardBoxSystem.cs @@ -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 _bodyQuery; + public override void Initialize() { base.Initialize(); + + _bodyQuery = GetEntityQuery(); + SubscribeNetworkEvent(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(ent, out var sprite)) diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs new file mode 100644 index 0000000000..5ba4878c6d --- /dev/null +++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -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 _appearanceQuery; + + public override void Initialize() + { + base.Initialize(); + + _appearanceQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + 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); + } +} diff --git a/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs new file mode 100644 index 0000000000..1586973a21 --- /dev/null +++ b/Content.Server/Polymorph/Systems/ChameleonProjectorSystem.cs @@ -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(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)); + } +} diff --git a/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs b/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs new file mode 100644 index 0000000000..2b9fba7b39 --- /dev/null +++ b/Content.Shared/Polymorph/Components/ChameleonDisguiseComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +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)] +public sealed partial class ChameleonDisguiseComponent : Component +{ + /// + /// The disguise source entity for copying the sprite. + /// + [DataField, AutoNetworkedField] + public EntityUid SourceEntity; + + /// + /// The source entity's prototype. + /// Used as a fallback if the source entity was deleted. + /// + [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 index 0000000000..239b5236f2 --- /dev/null +++ b/Content.Shared/Polymorph/Components/ChameleonProjectorComponent.cs @@ -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; + +/// +/// 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. +/// +[RegisterComponent, Access(typeof(SharedChameleonProjectorSystem))] +public sealed partial class ChameleonProjectorComponent : Component +{ + /// + /// If non-null, whitelist for valid entities to disguise as. + /// + [DataField(required: true)] + public EntityWhitelist? Whitelist; + + /// + /// If non-null, blacklist that prevents entities from being used even if they are in the whitelist. + /// + [DataField(required: true)] + public EntityWhitelist? Blacklist; + + /// + /// Polymorph configuration for the disguise entity. + /// + [DataField(required: true)] + public PolymorphConfiguration Polymorph = new(); + + /// + /// Action for disabling your disguise's rotation. + /// + [DataField] + public EntProtoId NoRotAction = "ActionDisguiseNoRot"; + + /// + /// Action for anchoring your disguise in place. + /// + [DataField] + public EntProtoId AnchorAction = "ActionDisguiseAnchor"; + + /// + /// Minimum health to give the disguise. + /// + [DataField] + public float MinHealth = 1f; + + /// + /// Maximum health to give the disguise, health scales with mass. + /// + [DataField] + public float MaxHealth = 100f; + + /// + /// Popup shown to the user when they try to disguise as an invalid entity. + /// + [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"; +} diff --git a/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs new file mode 100644 index 0000000000..c1abfc526f --- /dev/null +++ b/Content.Shared/Polymorph/Systems/SharedChameleonProjectorSystem.cs @@ -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; + +/// +/// Handles whitelist/blacklist checking. +/// Actual polymorphing and deactivation is done serverside. +/// +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(OnInteract); + } + + private void OnInteract(Entity 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); + } + + /// + /// Returns true if an entity cannot be used as a disguise. + /// + public bool IsInvalid(ChameleonProjectorComponent comp, EntityUid target) + { + return (comp.Whitelist?.IsValid(target, EntityManager) == false) + || (comp.Blacklist?.IsValid(target, EntityManager) == true); + } + + /// + /// On server, polymorphs the user into an entity and sets up the disguise. + /// + public virtual void Disguise(ChameleonProjectorComponent comp, EntityUid user, EntityUid entity) + { + } + + /// + /// Copy a component from the source entity/prototype to the disguise entity. + /// + /// + /// This would probably be a good thing to add to engine in the future. + /// + protected bool CopyComp(Entity ent) where T: Component, new() + { + if (!GetSrcComp(ent.Comp, out var src)) + return true; + + // remove then re-add to prevent a funny + RemComp(ent); + var dest = AddComp(ent); + _serMan.CopyTo(src, ref dest, notNullableOverride: true); + Dirty(ent, dest); + return false; + } + + /// + /// Try to get a single component from the source entity/prototype. + /// + private bool GetSrcComp(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(protoId, out var proto)) + return false; + + return proto.TryGetComponent(out src); + } +} + +/// +/// Action event for toggling transform NoRot on a disguise. +/// +public sealed partial class DisguiseToggleNoRotEvent : InstantActionEvent +{ +} + +/// +/// Action event for toggling transform Anchored on a disguise. +/// +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 index 0000000000..8a79516077 --- /dev/null +++ b/Resources/Locale/en-US/chameleon-projector/chameleon-projector.ftl @@ -0,0 +1,2 @@ +chameleon-projector-invalid = You can't disguise as that! +chameleon-projector-success = Projected new disguise. diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 8442725566..57a309ac69 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -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. diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index b1e6b88435..6375ac37f7 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -622,6 +622,16 @@ 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 index 0000000000..e021281021 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml @@ -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 index 0000000000000000000000000000000000000000..ce20b5eeeed277078a03893f96e9ce1aa8ca518f GIT binary patch literal 716 zcmV;-0yF)IP)Ve(f0`5$t{2 zQJ;5q<9(N=&Xx8j&rhk=oA+w(%T{E&!WVrn$v=!=SA{-zNXG1u${pY+O+`$ZuXxoKlP3u5%GM z`91sz_-pJp%s*`b{P_A^H5$`a6X&mp~tHosQe7JccUjrvo2PjrY0wSNYe}dd}~2_0jny z`Q#6fd4%xGnMeH6ssR3U@^w|`$o*x-XOn-GZ-P@V7;;tleDmdC`~J+`+%sSwZ-SE- zVWxhYXTW8Cm-zA5%bz;~6eMSqouEugDp91_%5;4do~!;$KvjNMT;s=IFMl*_g%(w& ytp2_W06+fYzyJI3eW&X_#`)JO=0C5^IGT8Tx_t4~ zX@6Nl{8v5ulh<%-62mLgwUz6yO>>#Rwqw3njO^a1`qCRp_ZvjpV=T? literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..1704b9c3c112fc423e9466328df5e2bac1f15c87 GIT binary patch literal 316 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9F5M?jcysy3fAQ1G3n zi(^Q|oVT|&@*Xk}alP2>ax#YJfu_-<Qq+r@Tz=cdWe;su{G zbQCijv1E88!%)cAV8QM%hgsnqqky@H!qP3Z{1Ny;?i_Y=X7}c^aI?Gd2{7O|KF>S%~kcJoh@js0_;4V~` bf53TkzSA$a-26*GpEG#6`njxgN@xNATZDFx literal 0 HcmV?d00001 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 index 0000000000..3eb42e9e6f --- /dev/null +++ b/Resources/Textures/Objects/Devices/chameleon_projector.rsi/meta.json @@ -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 + } + ] +}