continue;
}
+ var targetEv = new GetFlashEffectTargetEvent(ent);
+ RaiseLocalEvent(ent, ref targetEv);
+ ent = targetEv.Target;
+
EnsureComp<ColorFlashEffectComponent>(ent, out comp);
comp.NetSyncEnabled = false;
comp.Color = sprite.Color;
}
}
}
+
+/// <summary>
+/// Raised on an entity to change the target for a color flash effect.
+/// </summary>
+[ByRefEvent]
+public record struct GetFlashEffectTargetEvent(EntityUid Target);
+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;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
+ private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
+ _spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
+
+ SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentStartup>(OnStartup);
+ SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentShutdown>(OnShutdown);
+ SubscribeLocalEvent<ChameleonDisguisedComponent, GetFlashEffectTargetEvent>(OnGetFlashEffectTargetEvent);
}
private void OnHandleState(Entity<ChameleonDisguiseComponent> ent, ref AfterAutoHandleStateEvent args)
CopyComp<SpriteComponent>(ent);
CopyComp<GenericVisualizerComponent>(ent);
CopyComp<SolutionContainerVisualsComponent>(ent);
+ CopyComp<BurnStateVisualsComponent>(ent);
// reload appearance to hopefully prevent any invisible layers
if (_appearanceQuery.TryComp(ent, out var appearance))
_appearance.QueueUpdate(ent, appearance);
}
+
+ private void OnStartup(Entity<ChameleonDisguisedComponent> ent, ref ComponentStartup args)
+ {
+ if (!_spriteQuery.TryComp(ent, out var sprite))
+ return;
+
+ ent.Comp.WasVisible = sprite.Visible;
+ sprite.Visible = false;
+ }
+
+ private void OnShutdown(Entity<ChameleonDisguisedComponent> ent, ref ComponentShutdown args)
+ {
+ if (_spriteQuery.TryComp(ent, out var sprite))
+ sprite.Visible = ent.Comp.WasVisible;
+ }
+
+ private void OnGetFlashEffectTargetEvent(Entity<ChameleonDisguisedComponent> ent, ref GetFlashEffectTargetEvent args)
+ {
+ args.Target = ent.Comp.Disguise;
+ }
}
-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));
- }
-}
+public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem;
+using Content.Shared.Polymorph.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
/// Component added to disguise entities.
/// Used by client to copy over appearance from the disguise's source entity.
/// </summary>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedChameleonProjectorSystem))]
+[AutoGenerateComponentState(true)]
public sealed partial class ChameleonDisguiseComponent : Component
{
+ /// <summary>
+ /// The user of this disguise.
+ /// </summary>
+ [DataField]
+ public EntityUid User;
+
+ /// <summary>
+ /// The projector that created this disguise.
+ /// </summary>
+ [DataField]
+ public EntityUid Projector;
+
/// <summary>
/// The disguise source entity for copying the sprite.
/// </summary>
--- /dev/null
+using Content.Shared.Polymorph.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Polymorph.Components;
+
+/// <summary>
+/// Added to a player when they use a chameleon projector.
+/// Handles making them invisible and revealing when damaged enough or switching hands.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedChameleonProjectorSystem))]
+[AutoGenerateComponentState]
+public sealed partial class ChameleonDisguisedComponent : Component
+{
+ /// <summary>
+ /// The disguise entity parented to the player.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public EntityUid Disguise;
+
+ /// <summary>
+ /// For client, whether the user's sprite was previously visible or not.
+ /// </summary>
+ [DataField]
+ public bool WasVisible;
+}
-using Content.Shared.Polymorph;
using Content.Shared.Polymorph.Systems;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
public EntityWhitelist? Blacklist;
/// <summary>
- /// Polymorph configuration for the disguise entity.
+ /// Disguise entity to spawn and use.
/// </summary>
[DataField(required: true)]
- public PolymorphConfiguration Polymorph = new();
+ public EntProtoId DisguiseProto = string.Empty;
/// <summary>
/// Action for disabling your disguise's rotation.
/// </summary>
[DataField]
public EntProtoId NoRotAction = "ActionDisguiseNoRot";
+ [DataField]
+ public EntityUid? NoRotActionEntity;
/// <summary>
/// Action for anchoring your disguise in place.
/// </summary>
[DataField]
public EntProtoId AnchorAction = "ActionDisguiseAnchor";
+ [DataField]
+ public EntityUid? AnchorActionEntity;
/// <summary>
/// Minimum health to give the disguise.
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.
+ /// User currently disguised by this projector, if any
/// </summary>
[DataField]
- public LocId SuccessPopup = "chameleon-projector-success";
+ public EntityUid? Disguised;
}
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 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;
-using Content.Shared.Whitelist;
namespace Content.Shared.Polymorph.Systems;
/// <summary>
-/// Handles whitelist/blacklist checking.
-/// Actual polymorphing and deactivation is done serverside.
+/// Handles disguise validation, disguising and revealing.
+/// Most appearance copying is done clientside.
/// </summary>
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<ChameleonDisguiseComponent, InteractHandEvent>(OnDisguiseInteractHand, before: [typeof(SharedItemSystem)]);
+ SubscribeLocalEvent<ChameleonDisguiseComponent, DamageChangedEvent>(OnDisguiseDamaged);
+ SubscribeLocalEvent<ChameleonDisguiseComponent, InsertIntoEntityStorageAttemptEvent>(OnDisguiseInsertAttempt);
+ SubscribeLocalEvent<ChameleonDisguiseComponent, ComponentShutdown>(OnDisguiseShutdown);
+
SubscribeLocalEvent<ChameleonProjectorComponent, AfterInteractEvent>(OnInteract);
+ SubscribeLocalEvent<ChameleonProjectorComponent, GetVerbsEvent<UtilityVerb>>(OnGetVerbs);
+ SubscribeLocalEvent<ChameleonProjectorComponent, DisguiseToggleNoRotEvent>(OnToggleNoRot);
+ SubscribeLocalEvent<ChameleonProjectorComponent, DisguiseToggleAnchoredEvent>(OnToggleAnchored);
+ SubscribeLocalEvent<ChameleonProjectorComponent, HandDeselectedEvent>(OnDeselected);
+ SubscribeLocalEvent<ChameleonProjectorComponent, GotUnequippedHandEvent>(OnUnequipped);
+ SubscribeLocalEvent<ChameleonProjectorComponent, ComponentShutdown>(OnProjectorShutdown);
+ }
+
+ #region Disguise entity
+
+ private void OnDisguiseInteractHand(Entity<ChameleonDisguiseComponent> ent, ref InteractHandEvent args)
+ {
+ TryReveal(ent.Comp.User);
+ args.Handled = true;
}
+ private void OnDisguiseDamaged(Entity<ChameleonDisguiseComponent> ent, ref DamageChangedEvent args)
+ {
+ // this mirrors damage 1:1
+ if (args.DamageDelta is {} damage)
+ _damageable.TryChangeDamage(ent.Comp.User, damage);
+ }
+
+ private void OnDisguiseInsertAttempt(Entity<ChameleonDisguiseComponent> ent, ref InsertIntoEntityStorageAttemptEvent args)
+ {
+ // stay parented to the user, not the storage
+ args.Cancelled = true;
+ }
+
+ private void OnDisguiseShutdown(Entity<ChameleonDisguiseComponent> ent, ref ComponentShutdown args)
+ {
+ _actions.RemoveProvidedActions(ent.Comp.User, ent.Comp.Projector);
+ }
+
+ #endregion
+
+ #region Projector
+
private void OnInteract(Entity<ChameleonProjectorComponent> ent, ref AfterInteractEvent args)
{
- if (!args.CanReach || args.Target is not {} target)
+ if (args.Handled || !args.CanReach || args.Target is not {} target)
return;
- var user = args.User;
args.Handled = true;
+ TryDisguise(ent, args.User, target);
+ }
+
+ private void OnGetVerbs(Entity<ChameleonProjectorComponent> ent, ref GetVerbsEvent<UtilityVerb> args)
+ {
+ if (!args.CanAccess)
+ return;
+
+ var user = args.User;
+ 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<ChameleonProjectorComponent> 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<ChameleonProjectorComponent> 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<ChameleonProjectorComponent> 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<ChameleonProjectorComponent> ent, ref HandDeselectedEvent args)
+ {
+ RevealProjector(ent);
+ }
+
+ private void OnUnequipped(Entity<ChameleonProjectorComponent> ent, ref GotUnequippedHandEvent args)
+ {
+ RevealProjector(ent);
+ }
+
+ private void OnProjectorShutdown(Entity<ChameleonProjectorComponent> ent, ref ComponentShutdown args)
+ {
+ RevealProjector(ent);
+ }
+
+ #endregion
+
+ #region API
+
/// <summary>
/// Returns true if an entity cannot be used as a disguise.
/// </summary>
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);
}
/// <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)
+ public void Disguise(Entity<ChameleonProjectorComponent> 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<ChameleonDisguisedComponent>(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<ChameleonDisguiseComponent>(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<ItemComponent>((disguise, comp));
+
+ _appearance.CopyData(entity, disguise);
+ }
+
+ /// <summary>
+ /// Removes the disguise, if the user is disguised.
+ /// </summary>
+ public bool TryReveal(Entity<ChameleonDisguisedComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return false;
+
+ if (TryComp<ChameleonDisguiseComponent>(ent.Comp.Disguise, out var disguise)
+ && TryComp<ChameleonProjectorComponent>(disguise.Projector, out var proj))
+ {
+ proj.Disguised = null;
+ }
+
+ var xform = Transform(ent);
+ xform.NoLocalRotation = false;
+ _xform.Unanchor(ent, xform);
+
+ Del(ent.Comp.Disguise);
+ RemComp<ChameleonDisguisedComponent>(ent);
+ return true;
+ }
+
+ /// <summary>
+ /// Reveal a projector's user, if any.
+ /// </summary>
+ public void RevealProjector(Entity<ChameleonProjectorComponent> ent)
{
+ if (ent.Comp.Disguised is {} user)
+ TryReveal(user);
}
+ #endregion
+
/// <summary>
/// Copy a component from the source entity/prototype to the disguise entity.
/// </summary>
+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
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:
- 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
components:
- type: InstantAction
icon: Interface/VerbIcons/refresh.svg.192dpi.png
+ itemIconStyle: BigAction
event: !type:DisguiseToggleNoRotEvent
- type: entity
icon:
sprite: Objects/Tools/wrench.rsi
state: icon
+ itemIconStyle: BigAction
event: !type:DisguiseToggleAnchoredEvent