diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index db6bf409f9..5741ee1b8f 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -314,6 +314,9 @@ namespace Content.Client.Entry
"NukeCodePaper",
"GhostRadio",
"Armor",
+ "Guardian",
+ "GuardianCreator",
+ "GuardianHost",
"Udder",
"PneumaticCannon",
"Spreader",
diff --git a/Content.Server/Actions/Actions/GuardianToggleAction.cs b/Content.Server/Actions/Actions/GuardianToggleAction.cs
new file mode 100644
index 0000000000..5343e72284
--- /dev/null
+++ b/Content.Server/Actions/Actions/GuardianToggleAction.cs
@@ -0,0 +1,38 @@
+using Content.Server.Guardian;
+using Content.Shared.Actions.Behaviors;
+using Content.Shared.Cooldown;
+using Content.Shared.Popups;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Actions.Actions
+{
+ ///
+ /// Manifests the guardian saved in the action, using the system
+ ///
+ [UsedImplicitly]
+ [DataDefinition]
+ public class ToggleGuardianAction : IInstantAction
+ {
+ [DataField("cooldown")] public float Cooldown { get; [UsedImplicitly] private set; }
+
+ public void DoInstantAction(InstantActionEventArgs args)
+ {
+ var entManager = IoCManager.Resolve();
+
+ if (entManager.TryGetComponent(args.Performer, out GuardianHostComponent? hostComponent) &&
+ hostComponent.HostedGuardian != null)
+ {
+ EntitySystem.Get().ToggleGuardian(hostComponent);
+ args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
+ }
+ else
+ {
+ args.Performer.PopupMessage(Loc.GetString("guardian-missing-invalid-action"));
+ }
+ }
+ }
+}
diff --git a/Content.Server/Guardian/GuardianComponent.cs b/Content.Server/Guardian/GuardianComponent.cs
new file mode 100644
index 0000000000..a8ebedf22c
--- /dev/null
+++ b/Content.Server/Guardian/GuardianComponent.cs
@@ -0,0 +1,39 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Guardian
+{
+ ///
+ /// Given to guardians to monitor their link with the host
+ ///
+ [RegisterComponent]
+ [ComponentProtoName("Guardian")]
+ public class GuardianComponent : Component
+ {
+ ///
+ /// The guardian host entity
+ ///
+ public EntityUid Host;
+
+ ///
+ /// Percentage of damage reflected from the guardian to the host
+ ///
+ [ViewVariables]
+ [DataField("damageShare")]
+ public float DamageShare { get; set; } = 0.85f;
+
+ ///
+ /// Maximum distance the guardian can travel before it's forced to recall, use YAML to set
+ ///
+ [ViewVariables]
+ [DataField("distanceAllowed")]
+ public float DistanceAllowed { get; set; } = 5f;
+
+ ///
+ /// If the guardian is currently manifested
+ ///
+ public bool GuardianLoose = false;
+
+ }
+}
diff --git a/Content.Server/Guardian/GuardianCreatorComponent.cs b/Content.Server/Guardian/GuardianCreatorComponent.cs
new file mode 100644
index 0000000000..d581ddc351
--- /dev/null
+++ b/Content.Server/Guardian/GuardianCreatorComponent.cs
@@ -0,0 +1,39 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Guardian
+{
+ ///
+ /// Creates a GuardianComponent attached to the user's GuardianHost.
+ ///
+ [RegisterComponent]
+ [ComponentProtoName("GuardianCreator")]
+ public sealed class GuardianCreatorComponent : Component
+ {
+ ///
+ /// Counts as spent upon exhausting the injection
+ ///
+ ///
+ /// We don't mark as deleted as examine depends on this.
+ ///
+ public bool Used = false;
+
+ ///
+ /// The prototype of the guardian entity which will be created
+ ///
+ [ViewVariables]
+ [DataField("guardianProto", customTypeSerializer:typeof(PrototypeIdSerializer), required: true)]
+ public string GuardianProto { get; set; } = default!;
+
+ ///
+ /// How long it takes to inject someone.
+ ///
+ [DataField("delay")]
+ public float InjectionDelay = 5f;
+
+ public bool Injecting = false;
+ }
+}
diff --git a/Content.Server/Guardian/GuardianHostComponent.cs b/Content.Server/Guardian/GuardianHostComponent.cs
new file mode 100644
index 0000000000..e9aecdae2a
--- /dev/null
+++ b/Content.Server/Guardian/GuardianHostComponent.cs
@@ -0,0 +1,27 @@
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.Guardian
+{
+ ///
+ /// Given to guardian users upon establishing a guardian link with the entity
+ ///
+ [RegisterComponent]
+ [ComponentProtoName("GuardianHost")]
+ public sealed class GuardianHostComponent : Component
+ {
+ ///
+ /// Guardian hosted within the component
+ ///
+ ///
+ /// Can be null if the component is added at any time.
+ ///
+ public EntityUid? HostedGuardian;
+
+ ///
+ /// Container which holds the guardian
+ ///
+ [ViewVariables] public ContainerSlot GuardianContainer = default!;
+ }
+}
diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs
new file mode 100644
index 0000000000..7373a6c079
--- /dev/null
+++ b/Content.Server/Guardian/GuardianSystem.cs
@@ -0,0 +1,349 @@
+using Content.Server.Actions;
+using Content.Server.DoAfter;
+using Content.Server.Hands.Components;
+using Content.Server.Inventory.Components;
+using Content.Server.Popups;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
+using Content.Shared.Audio;
+using Content.Shared.Damage;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.MobState;
+using Content.Shared.MobState.EntitySystems;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Log;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Guardian
+{
+ ///
+ /// A guardian has a host it's attached to that it fights for. A fighting spirit.
+ ///
+ public sealed class GuardianSystem : EntitySystem
+ {
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnCreatorUse);
+ SubscribeLocalEvent(OnCreatorInteract);
+ SubscribeLocalEvent(OnCreatorExamine);
+ SubscribeLocalEvent(OnCreatorInject);
+ SubscribeLocalEvent(OnCreatorCancelled);
+
+ SubscribeLocalEvent(OnGuardianMove);
+ SubscribeLocalEvent(OnGuardianDamaged);
+ SubscribeLocalEvent(OnGuardianPlayer);
+ SubscribeLocalEvent(OnGuardianUnplayer);
+
+ SubscribeLocalEvent(OnHostInit);
+ SubscribeLocalEvent(OnHostMove);
+ SubscribeLocalEvent(OnHostStateChange);
+ SubscribeLocalEvent(OnHostShutdown);
+ }
+
+ private void OnGuardianUnplayer(EntityUid uid, GuardianComponent component, PlayerDetachedEvent args)
+ {
+ var host = component.Host;
+
+ if (!TryComp(host, out var hostComponent)) return;
+
+ RetractGuardian(hostComponent, component);
+ }
+
+ private void OnGuardianPlayer(EntityUid uid, GuardianComponent component, PlayerAttachedEvent args)
+ {
+ var host = component.Host;
+
+ if (!HasComp(host)) return;
+
+ _popupSystem.PopupEntity(Loc.GetString("guardian-available"), host, Filter.Entities(host));
+ }
+
+ private void OnHostInit(EntityUid uid, GuardianHostComponent component, ComponentInit args)
+ {
+ component.GuardianContainer = uid.EnsureContainer("GuardianContainer");
+ }
+
+ private void OnHostShutdown(EntityUid uid, GuardianHostComponent component, ComponentShutdown args)
+ {
+ if (component.HostedGuardian == null) return;
+ EntityManager.QueueDeleteEntity(component.HostedGuardian.Value);
+ }
+
+ public void ToggleGuardian(GuardianHostComponent hostComponent)
+ {
+ if (hostComponent.HostedGuardian == null ||
+ !TryComp(hostComponent.HostedGuardian, out GuardianComponent? guardianComponent)) return;
+
+ if (guardianComponent.GuardianLoose)
+ {
+ RetractGuardian(hostComponent, guardianComponent);
+ }
+ else
+ {
+ ReleaseGuardian(hostComponent, guardianComponent);
+ }
+ }
+
+ ///
+ /// Adds the guardian host component to the user and spawns the guardian inside said component
+ ///
+ private void OnCreatorUse(EntityUid uid, GuardianCreatorComponent component, UseInHandEvent args)
+ {
+ if (args.Handled) return;
+ args.Handled = true;
+ UseCreator(args.User, args.User, component);
+ }
+
+ private void OnCreatorInteract(EntityUid uid, GuardianCreatorComponent component, AfterInteractEvent args)
+ {
+ if (args.Handled || args.Target == null) return;
+ args.Handled = true;
+ UseCreator(args.User, args.Target.Value, component);
+ }
+
+ private void OnCreatorCancelled(GuardianCreatorInjectCancelledEvent ev)
+ {
+ ev.Component.Injecting = false;
+ }
+
+ private void UseCreator(EntityUid user, EntityUid target, GuardianCreatorComponent component)
+ {
+ if (component.Used)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("guardian-activator-empty-invalid-creation"), user, Filter.Entities(user));
+ return;
+ }
+
+ // If user is already a host don't duplicate.
+ if (HasComp(target))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("guardian-already-present-invalid-creation"), user, Filter.Entities(user));
+ return;
+ }
+
+ // Can't work without actions
+ EntityManager.EnsureComponent(target);
+
+ if (component.Injecting) return;
+
+ component.Injecting = true;
+
+ _doAfterSystem.DoAfter(new DoAfterEventArgs(user, component.InjectionDelay, target: target)
+ {
+ BroadcastFinishedEvent = new GuardianCreatorInjectedEvent(user, target, component),
+ BroadcastCancelledEvent = new GuardianCreatorInjectCancelledEvent(target, component),
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ });
+ }
+
+ private void OnCreatorInject(GuardianCreatorInjectedEvent ev)
+ {
+ var comp = ev.Component;
+
+ if (comp.Deleted ||
+ comp.Used ||
+ !TryComp(ev.User, out var hands) ||
+ !hands.IsHolding(comp.Owner) ||
+ HasComp(ev.Target) ||
+ !TryComp(ev.Target, out var actions))
+ {
+ comp.Injecting = false;
+ return;
+ }
+
+ var hostXform = EntityManager.GetComponent(ev.Target);
+ var host = EntityManager.EnsureComponent(ev.Target);
+ // Use map position so it's not inadvertantly parented to the host + if it's in a container it spawns outside I guess.
+ var guardian = EntityManager.SpawnEntity(comp.GuardianProto, hostXform.MapPosition);
+
+ host.GuardianContainer.Insert(guardian);
+ host.HostedGuardian = guardian;
+
+ if (TryComp(guardian, out GuardianComponent? guardianComponent))
+ {
+ guardianComponent.Host = ev.Target;
+
+ // Grant the user the recall action and notify them
+ actions.Grant(ActionType.ManifestGuardian);
+ SoundSystem.Play(Filter.Entities(ev.Target), "/Audio/Effects/guardian_inject.ogg", ev.Target);
+
+ _popupSystem.PopupEntity(Loc.GetString("guardian-created"), ev.Target, Filter.Entities(ev.Target));
+ // Exhaust the activator
+ comp.Used = true;
+ }
+ else
+ {
+ Logger.ErrorS("guardian", $"Tried to spawn a guardian that doesn't have {nameof(GuardianComponent)}");
+ EntityManager.QueueDeleteEntity(guardian);
+ }
+ }
+
+ ///
+ /// Triggers when the host receives damage which puts the host in either critical or killed state
+ ///
+ private void OnHostStateChange(EntityUid uid, GuardianHostComponent component, MobStateChangedEvent args)
+ {
+ if (component.HostedGuardian == null) return;
+
+ if (args.CurrentMobState.IsCritical())
+ {
+ _popupSystem.PopupEntity(Loc.GetString("guardian-critical-warn"), component.HostedGuardian.Value, Filter.Entities(component.HostedGuardian.Value));
+ SoundSystem.Play(Filter.Entities(component.HostedGuardian.Value), "/Audio/Effects/guardian_warn.ogg", component.HostedGuardian.Value);
+ }
+ else if (args.CurrentMobState.IsDead())
+ {
+ SoundSystem.Play(Filter.Pvs(uid), "/Audio/Voice/Human/malescream_guardian.ogg", uid, AudioHelpers.WithVariation(0.20f));
+ EntityManager.RemoveComponent(uid);
+ }
+ }
+
+ ///
+ /// Handles guardian receiving damage and splitting it with the host according to his defence percent
+ ///
+ private void OnGuardianDamaged(EntityUid uid, GuardianComponent component, DamageChangedEvent args)
+ {
+ if (args.DamageDelta == null) return;
+
+ _damageSystem.TryChangeDamage(component.Host, args.DamageDelta * component.DamageShare);
+ _popupSystem.PopupEntity(Loc.GetString("guardian-entity-taking-damage"), component.Host, Filter.Entities(component.Host));
+
+ }
+
+ ///
+ /// Triggers while trying to examine an activator to see if it's used
+ ///
+ private void OnCreatorExamine(EntityUid uid, GuardianCreatorComponent component, ExaminedEvent args)
+ {
+ if (component.Used)
+ {
+ args.PushMarkup(Loc.GetString("guardian-activator-empty-examine"));
+ }
+ }
+
+ ///
+ /// Called every time the host moves, to make sure the distance between the host and the guardian isn't too far
+ ///
+ private void OnHostMove(EntityUid uid, GuardianHostComponent component, ref MoveEvent args)
+ {
+ if (component.HostedGuardian == null ||
+ !TryComp(component.HostedGuardian, out GuardianComponent? guardianComponent) ||
+ !guardianComponent.GuardianLoose) return;
+
+ CheckGuardianMove(uid, component.HostedGuardian.Value, component);
+ }
+
+ ///
+ /// Called every time the guardian moves: makes sure it's not out of it's allowed distance
+ ///
+ private void OnGuardianMove(EntityUid uid, GuardianComponent component, ref MoveEvent args)
+ {
+ if (!component.GuardianLoose) return;
+
+ CheckGuardianMove(component.Host, uid, guardianComponent: component);
+ }
+
+ ///
+ /// Retract the guardian if either the host or the guardian move away from each other.
+ ///
+ private void CheckGuardianMove(
+ EntityUid hostUid,
+ EntityUid guardianUid,
+ GuardianHostComponent? hostComponent = null,
+ GuardianComponent? guardianComponent = null,
+ TransformComponent? hostXform = null,
+ TransformComponent? guardianXform = null)
+ {
+ if (!Resolve(hostUid, ref hostComponent, ref hostXform) ||
+ !Resolve(guardianUid, ref guardianComponent, ref guardianXform))
+ {
+ return;
+ }
+
+ if (!guardianComponent.GuardianLoose) return;
+
+ if (!guardianXform.Coordinates.InRange(EntityManager, hostXform.Coordinates, guardianComponent.DistanceAllowed))
+ {
+ RetractGuardian(hostComponent, guardianComponent);
+ }
+ }
+
+ private bool CanRelease(GuardianHostComponent host, GuardianComponent guardian)
+ {
+ return HasComp(guardian.Owner);
+ }
+
+ private void ReleaseGuardian(GuardianHostComponent hostComponent, GuardianComponent guardianComponent)
+ {
+ if (guardianComponent.GuardianLoose)
+ {
+ DebugTools.Assert(!hostComponent.GuardianContainer.Contains(guardianComponent.Owner));
+ return;
+ }
+
+ if (!CanRelease(hostComponent, guardianComponent))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("guardian-no-soul"), hostComponent.Owner, Filter.Entities(hostComponent.Owner));
+ return;
+ }
+
+ DebugTools.Assert(hostComponent.GuardianContainer.Contains(guardianComponent.Owner));
+ hostComponent.GuardianContainer.Remove(guardianComponent.Owner);
+ DebugTools.Assert(!hostComponent.GuardianContainer.Contains(guardianComponent.Owner));
+
+ guardianComponent.GuardianLoose = true;
+ }
+
+ private void RetractGuardian(GuardianHostComponent hostComponent, GuardianComponent guardianComponent)
+ {
+ if (!guardianComponent.GuardianLoose)
+ {
+ DebugTools.Assert(hostComponent.GuardianContainer.Contains(guardianComponent.Owner));
+ return;
+ }
+
+ hostComponent.GuardianContainer.Insert(guardianComponent.Owner);
+ DebugTools.Assert(hostComponent.GuardianContainer.Contains(guardianComponent.Owner));
+ _popupSystem.PopupEntity(Loc.GetString("guardian-entity-recall"), hostComponent.Owner, Filter.Pvs(hostComponent.Owner));
+ guardianComponent.GuardianLoose = false;
+ }
+
+ private sealed class GuardianCreatorInjectedEvent : EntityEventArgs
+ {
+ public EntityUid User { get; }
+ public EntityUid Target { get; }
+ public GuardianCreatorComponent Component { get; }
+
+ public GuardianCreatorInjectedEvent(EntityUid user, EntityUid target, GuardianCreatorComponent component)
+ {
+ User = user;
+ Target = target;
+ Component = component;
+ }
+ }
+
+ private sealed class GuardianCreatorInjectCancelledEvent : EntityEventArgs
+ {
+ public EntityUid Target { get; }
+ public GuardianCreatorComponent Component { get; }
+
+ public GuardianCreatorInjectCancelledEvent(EntityUid target, GuardianCreatorComponent component)
+ {
+ Target = target;
+ Component = component;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Actions/ActionType.cs b/Content.Shared/Actions/ActionType.cs
index 0cb60f42f0..4eb19eaf34 100644
--- a/Content.Shared/Actions/ActionType.cs
+++ b/Content.Shared/Actions/ActionType.cs
@@ -18,6 +18,7 @@ namespace Content.Shared.Actions
DebugTargetEntity,
DebugTargetEntityRepeat,
SpellPie,
+ ManifestGuardian,
PAIMidi
}
diff --git a/Content.Shared/MobState/Components/MobStateComponent.cs b/Content.Shared/MobState/Components/MobStateComponent.cs
index 697849abe5..90f9dfeaed 100644
--- a/Content.Shared/MobState/Components/MobStateComponent.cs
+++ b/Content.Shared/MobState/Components/MobStateComponent.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
+using Content.Shared.MobState.EntitySystems;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -318,7 +319,6 @@ namespace Content.Shared.MobState.Components
var message = new MobStateChangedEvent(this, old, state);
entMan.EventBus.RaiseLocalEvent(Owner, message);
-
Dirty();
}
}
diff --git a/Content.Shared/MobState/EntitySystems/MobStateSystem.cs b/Content.Shared/MobState/EntitySystems/MobStateSystem.cs
index 91dac34ca2..c4c2126f4d 100644
--- a/Content.Shared/MobState/EntitySystems/MobStateSystem.cs
+++ b/Content.Shared/MobState/EntitySystems/MobStateSystem.cs
@@ -111,7 +111,7 @@ namespace Content.Shared.MobState.EntitySystems
private void OnStartPullAttempt(EntityUid uid, MobStateComponent component, StartPullAttemptEvent args)
{
- if(component.IsIncapacitated())
+ if (component.IsIncapacitated())
args.Cancel();
}
@@ -135,7 +135,7 @@ namespace Content.Shared.MobState.EntitySystems
private void OnStandAttempt(EntityUid uid, MobStateComponent component, StandAttemptEvent args)
{
- if(component.IsIncapacitated())
+ if (component.IsIncapacitated())
args.Cancel();
}
}
diff --git a/Resources/Audio/Effects/guardian_inject.ogg b/Resources/Audio/Effects/guardian_inject.ogg
new file mode 100644
index 0000000000..3dca7114a1
Binary files /dev/null and b/Resources/Audio/Effects/guardian_inject.ogg differ
diff --git a/Resources/Audio/Effects/guardian_warn.ogg b/Resources/Audio/Effects/guardian_warn.ogg
new file mode 100644
index 0000000000..617563b644
Binary files /dev/null and b/Resources/Audio/Effects/guardian_warn.ogg differ
diff --git a/Resources/Audio/Voice/Human/malescream_guardian.ogg b/Resources/Audio/Voice/Human/malescream_guardian.ogg
new file mode 100644
index 0000000000..a854335da7
Binary files /dev/null and b/Resources/Audio/Voice/Human/malescream_guardian.ogg differ
diff --git a/Resources/Locale/en-US/guardian/guardian.ftl b/Resources/Locale/en-US/guardian/guardian.ftl
new file mode 100644
index 0000000000..7aca501bd2
--- /dev/null
+++ b/Resources/Locale/en-US/guardian/guardian.ftl
@@ -0,0 +1,22 @@
+
+## Guardian host specific
+
+guardian-created = You feel... Haunted.
+guardian-missing-invalid-action = There is no guardian under your control!
+guardian-already-present-invalid-creation = You are NOT re-living that haunting experience!
+guardian-no-actions-invalid-creation = You don't have the ability to host a guardian!
+guardian-activator-empty-invalid-creation = The injector is spent.
+guardian-activator-empty-examine = [color=#ba1919]The injector is spent.[/color]
+guardian-no-soul = Your guardian has no soul.
+guardian-available = Your guardian now has a soul.
+
+## Guardian entity specific
+
+guardian-entity-recall = The guardian vanishes into thin air!
+guardian-entity-taking-damage = Your guardian is taking damage!
+
+## Health warnings
+guardian-host-critical-warn = YOUR HOST IS WOUNDED!
+guardian-host-death-warn = YOUR FORM SUCCUMBS TO NONEXISTENCE!
+guardian-death-warn = YOUR BODY IS PIERCED BY SUBATOMIC PAIN AS IT DISINTEGRATES!
+
diff --git a/Resources/Prototypes/Actions/guardian_actions.yml b/Resources/Prototypes/Actions/guardian_actions.yml
new file mode 100644
index 0000000000..1725cf62b8
--- /dev/null
+++ b/Resources/Prototypes/Actions/guardian_actions.yml
@@ -0,0 +1,12 @@
+#This is the action the host gets to control the guardian
+- type: action
+ actionType: ManifestGuardian
+ icon: Interface/Actions/manifest.png
+ name: "Toggle guardian manifestation"
+ description: "Either manifests the guardian or recalls it back into your body"
+ filters:
+ - guardian
+ behaviorType: Instant
+ behavior: !type:ToggleGuardianAction
+ cooldown: 2
+
diff --git a/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml b/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml
index d0ed9631a2..4978d16bb0 100644
--- a/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml
+++ b/Resources/Prototypes/Catalog/Fills/Paper/manuals.yml
@@ -16,3 +16,32 @@
Higher will burn the engine out and eventually make it explode. Don't.
Don't forget to refuel it, it tends to stop at the worst possible time.
+- type: entity
+ parent: BaseItem
+ id: HoloparasiteInfo
+ name: "Holoparasite terms and conditions"
+ description: A tiny volumetric display for documents, makes one wonder if Cybersun's legal budget is way too high.
+ components:
+ - type: Sprite
+ netsync: false
+ sprite: Objects/Misc/guardian_info.rsi
+ state: guardian_info
+ - type: UserInterface
+ interfaces:
+ - key: enum.PaperUiKey.Key
+ type: PaperBoundUserInterface
+ - type: Paper
+ content: |
+ Thanks for choosing our holoparasite package!
+ At cybersun, we pride ourselves on cutting-edge military and industrial technology, and greatly appreciate your contribution to our establishment!
+ Guardians are helpful and intelligent beings which nest within your body, completely immune to common hazards such as pressure, temperature and even bullets!
+ You have purchased the holoparasite package, which contains a holoparasite activator, an instruction booklet, and our softcap merchandise.
+ Instructions for use:
+ 1. Activate the holoparasite injector (preferably in a secluded area).
+ 2. Wait for the tingling and/or painful metaphysical sensation.
+ 3. Check your holoparasite for the ability to communicate and cooperate, and capacity to understand your orders.
+ 4. Use your recall-manifest ability to summon or recall the holoparasite back into your body.
+ 5. Keep the holoparasite within a short distance from yourself, otherwise it will be recalled by force!
+ WARNING: Guardians are metaphysical beings, but draw from your HEALTH in order to exist. Direct damage done to guardians will be partially transferred to you!
+ Cybersun inc. is not responsible for complete annihilation following the misuse of Holoparasite technology.
+
diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml
index 776635b834..56c4b6281e 100644
--- a/Resources/Prototypes/Catalog/uplink_catalog.yml
+++ b/Resources/Prototypes/Catalog/uplink_catalog.yml
@@ -107,6 +107,18 @@
price: 2
icon: /Textures/Objects/Weapons/Guns/Ammunition/SpeedLoaders/Magnum/magnum_sl.rsi/icon.png
+#Utility
+
+- type: uplinkListing
+ id: UplinkHoloparaKit
+ category: Utility
+ itemId: BoxHoloparasite
+ listingName: Holoparasite Kit
+ description: The pride and joy of Cybersun. Contains an injector that hosts a sentient metaphysical guardian made of hard light which resides in the user's body when not active. The guardian can punch rapidly and is immune to hazardous environments and bullets, but shares any damage it takes with the user.
+ icon: /Textures/Objects/Misc/guardian_info.rsi/icon.png
+ price: 16
+
+
# Bundles
- type: uplinkListing
@@ -209,6 +221,8 @@
id: UplinkDecoyDisk
category: Misc
itemId: NukeDiskFake
+ listingName: Decoy nuclear disk
+ description: A piece of plastic with a lenticular printing, made to look like a nuclear auth disk.
price: 1
- type: uplinkListing
@@ -235,16 +249,14 @@
itemId: lanternextrabright
price: 2
-# Pointless
-
- type: uplinkListing
id: UplinkCostumeCentcom
- category: Pointless
+ category: Misc
itemId: ClothingBackpackDuffelSyndicateCostumeCentcom
price: 4
- type: uplinkListing
id: UplinkCostumeClown
- category: Pointless
+ category: Misc
itemId: ClothingBackpackDuffelSyndicateCostumeClown
price: 4
diff --git a/Resources/Prototypes/Entities/Clothing/Head/soft.yml b/Resources/Prototypes/Entities/Clothing/Head/soft.yml
index a3d22355dc..7eff28e36c 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/soft.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/soft.yml
@@ -239,3 +239,25 @@
sprite: Clothing/Head/Soft/yellowsoft_flipped.rsi
- type: Clothing
sprite: Clothing/Head/Soft/yellowsoft_flipped.rsi
+
+- type: entity
+ parent: ClothingHeadBase
+ id: ClothingHeadHatBizarreSoft
+ name: troublemaker's soft
+ description: A truly.. bizarre accessory.
+ components:
+ - type: Sprite
+ sprite: Clothing/Head/Soft/bizarresoft.rsi
+ - type: Clothing
+ sprite: Clothing/Head/Soft/bizarresoft.rsi
+
+- type: entity
+ parent: ClothingHeadBase
+ id: ClothingHeadHatBizarreSoftFlipped
+ name: troublemaker's soft flipped
+ description: A truly.. bizarre accessory, flipped.
+ components:
+ - type: Sprite
+ sprite: Clothing/Head/Soft/bizarresoft_flipped.rsi
+ - type: Clothing
+ sprite: Clothing/Head/Soft/bizarresoft_flipped.rsi
diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
new file mode 100644
index 0000000000..0261ad243b
--- /dev/null
+++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
@@ -0,0 +1,73 @@
+# Does not inherit from simplemob
+- type: entity
+ name: Holoparasite
+ id: MobHoloparasite
+ description: A mesmerising whirl of hard-light patterns weaves a marvelous, yet oddly familiar visage. It stands proud, tuning into its owner's life to sustain itself.
+ components:
+ - type: GhostTakeoverAvailable
+ makeSentient: true
+ name: Holoparasite
+ description: Listen to your owner. Don't tank damage. Punch people hard.
+ - type: Input
+ context: "human"
+ - type: PlayerMobMover
+ - type: PlayerInputMover
+ - type: MovementSpeedModifier
+ baseWalkSpeed : 7
+ baseSprintSpeed : 7
+ - type: DamageOnHighSpeedImpact
+ damage:
+ types:
+ Blunt: 5
+ soundHit:
+ path: /Audio/Effects/hit_kick.ogg
+ # TODO: Randomise sprites and randomise the layer color
+ - type: Sprite
+ drawdepth: Mobs
+ sprite: Mobs/Aliens/Guardians/guardians.rsi
+ layers:
+ - state: tech_base
+ - state: tech_flare
+ color: "#40a7d7"
+ shader: unshaded
+ noRot: true
+ - type: Clickable
+ - type: InteractionOutline
+ - type: Physics
+ bodyType: KinematicController
+ - type: Fixtures
+ fixtures:
+ - shape:
+ !type:PhysShapeCircle
+ radius: 0.35
+ mass: 10
+ mask:
+ - Impassable
+ - SmallImpassable
+ - MobImpassable
+ layer:
+ - Opaque
+ - type: Damageable
+ damageContainer: Biological
+ - type: MobState
+ thresholds:
+ 0: !type:NormalMobState {}
+ - type: HeatResistance
+ - type: CombatMode
+ - type: Internals
+ - type: Examiner
+ - type: Speech
+ - type: Pullable
+ - type: UnarmedCombat
+ range: 2
+ arcwidth: 30
+ arc: fist
+ cooldownTime: 0.7
+ arcCooldownTime: 0.7
+ damage:
+ types:
+ Blunt: 40
+ - type: Actions
+ innateActions:
+ - CombatMode
+ - type: Guardian
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/guardian_activators.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/guardian_activators.yml
new file mode 100644
index 0000000000..37ff027f9a
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/guardian_activators.yml
@@ -0,0 +1,26 @@
+- type: entity
+ name: holoparasite injector
+ id: HoloparasiteInjector
+ parent: BaseItem
+ description: A complex artwork of handheld machinery allowing the user to host a holoparasite guardian.
+ components:
+ - type: Sprite
+ sprite: Objects/Specific/Medical/hypospray.rsi
+ state: combat_hypo
+ netsync: false
+ - type: GuardianCreator
+ guardianProto: MobHoloparasite
+
+- type: entity
+ name: holoparasite box
+ parent: BoxBase
+ id: BoxHoloparasite
+ description: A box containing a holoparasite injector
+ components:
+ - type: StorageFill
+ contents:
+ - id: HoloparasiteInjector
+ - id: HoloparasiteInfo
+ - type: Sprite
+ layers:
+ - state: box
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/equipped-HELMET.png
new file mode 100644
index 0000000000..210ee84379
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/equipped-HELMET.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/icon.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/icon.png
new file mode 100644
index 0000000000..a9b5f6decb
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/icon.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/inhand-left.png
new file mode 100644
index 0000000000..4cec5427a9
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/inhand-right.png
new file mode 100644
index 0000000000..4e0fb56872
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/meta.json b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/meta.json
new file mode 100644
index 0000000000..b3513bb5cd
--- /dev/null
+++ b/Resources/Textures/Clothing/Head/Soft/bizarresoft.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-NC-SA-3.0",
+ "copyright": "Taken from civstation at commit https://github.com/Civ13/Civ13/commit/ec52cbb95d59b717d4d8c480b35ac133e5b58088#diff-fba188fb2db5d16e5d41985147336a8f96085f761b903e016fffd869b63e497d",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "equipped-HELMET",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/equipped-HELMET.png
new file mode 100644
index 0000000000..fc6c43c930
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/equipped-HELMET.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/icon.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/icon.png
new file mode 100644
index 0000000000..bb759bf4f2
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/icon.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/inhand-left.png
new file mode 100644
index 0000000000..4cec5427a9
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/inhand-right.png
new file mode 100644
index 0000000000..ec0fb5b2c8
Binary files /dev/null and b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/meta.json b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/meta.json
new file mode 100644
index 0000000000..b3513bb5cd
--- /dev/null
+++ b/Resources/Textures/Clothing/Head/Soft/bizarresoft_flipped.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-NC-SA-3.0",
+ "copyright": "Taken from civstation at commit https://github.com/Civ13/Civ13/commit/ec52cbb95d59b717d4d8c480b35ac133e5b58088#diff-fba188fb2db5d16e5d41985147336a8f96085f761b903e016fffd869b63e497d",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "equipped-HELMET",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Interface/Actions/manifest.png b/Resources/Textures/Interface/Actions/manifest.png
new file mode 100644
index 0000000000..7db153e9f2
Binary files /dev/null and b/Resources/Textures/Interface/Actions/manifest.png differ
diff --git a/Resources/Textures/Interface/Actions/meta.json b/Resources/Textures/Interface/Actions/meta.json
index ad41346ff5..316ef865c0 100644
--- a/Resources/Textures/Interface/Actions/meta.json
+++ b/Resources/Textures/Interface/Actions/meta.json
@@ -21,6 +21,9 @@
},
{
"name": "harm"
+ },
+ {
+ "name": "manifest"
}
]
}
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic.png
new file mode 100644
index 0000000000..245d9d35c2
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic_base.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic_base.png
new file mode 100644
index 0000000000..cca7a46c34
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic_base.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic_flare.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic_flare.png
new file mode 100644
index 0000000000..3639229c4f
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/magic_flare.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/meta.json b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/meta.json
new file mode 100644
index 0000000000..28c17164ad
--- /dev/null
+++ b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/meta.json
@@ -0,0 +1,57 @@
+{
+ "version": 1,
+ "license": "CC-BY-NC-SA-3.0",
+ "copyright": "taken from /tg/ station at commit https://github.com/tgstation/tgstation/commit/02756c2bc2cf3000080d030955e994242bab39b5",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "magic_flare",
+ "directions": 4
+ },
+ {
+ "name": "magic_base",
+ "directions": 4
+ },
+ {
+ "name": "miner_flare",
+ "directions": 4
+ },
+ {
+ "name": "miner_base",
+ "directions": 4,
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1
+ ],
+ [
+ 0.1,
+ 0.1,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "tech_flare",
+ "directions": 4
+ },
+ {
+ "name": "tech_base",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner.png
new file mode 100644
index 0000000000..f4dd065628
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner_base.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner_base.png
new file mode 100644
index 0000000000..f22a027edc
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner_base.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner_flare.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner_flare.png
new file mode 100644
index 0000000000..73d1ba44d9
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/miner_flare.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech.png
new file mode 100644
index 0000000000..ada4a5ca14
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech_base.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech_base.png
new file mode 100644
index 0000000000..3a0a597254
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech_base.png differ
diff --git a/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech_flare.png b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech_flare.png
new file mode 100644
index 0000000000..6a38a65f70
Binary files /dev/null and b/Resources/Textures/Mobs/Aliens/Guardians/guardians.rsi/tech_flare.png differ
diff --git a/Resources/Textures/Objects/Misc/guardian_info.rsi/guardian_info.png b/Resources/Textures/Objects/Misc/guardian_info.rsi/guardian_info.png
new file mode 100644
index 0000000000..8de21319c1
Binary files /dev/null and b/Resources/Textures/Objects/Misc/guardian_info.rsi/guardian_info.png differ
diff --git a/Resources/Textures/Objects/Misc/guardian_info.rsi/icon.png b/Resources/Textures/Objects/Misc/guardian_info.rsi/icon.png
new file mode 100644
index 0000000000..ec276cc88d
Binary files /dev/null and b/Resources/Textures/Objects/Misc/guardian_info.rsi/icon.png differ
diff --git a/Resources/Textures/Objects/Misc/guardian_info.rsi/meta.json b/Resources/Textures/Objects/Misc/guardian_info.rsi/meta.json
new file mode 100644
index 0000000000..e65546a6b7
--- /dev/null
+++ b/Resources/Textures/Objects/Misc/guardian_info.rsi/meta.json
@@ -0,0 +1,25 @@
+{
+ "version": 1,
+ "license": "CC-BY-NC-SA-4.0",
+ "copyright": "original asset provided by https://github.com/CrudeWax",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "guardian_info",
+ "delays": [
+ [
+ 0.2,
+ 0.1,
+ 0.2,
+ 0.1
+ ]
+ ]
+ },
+ {
+ "name": "icon"
+ }
+ ]
+}