]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Adds HugBot (#37557)
authorCentronias <charlie.t.santos@gmail.com>
Sat, 11 Oct 2025 00:51:12 +0000 (17:51 -0700)
committerGitHub <noreply@github.com>
Sat, 11 Oct 2025 00:51:12 +0000 (00:51 +0000)
* - hugbot
  - bdy with two arms because it needs two arms to hug
  - is constructable from:
    - box of hugs
    - proximity sensor
    - two borg arms
  - lots of voice lines
  - kinda like a medibot, it chases you down and then hugs you
    - except if it's emagged, then it punches you :)
    - it has a 2m cooldown per person by default

- MeleeAttackOperator
  - Read the doc, but it's an operator which makes the NPC hit a target exactly once assuming it's in range.
  - Used to make the hugbot attack
- RaiseEventForOwnerOperator
  - Read the doc, but it's an operator which raises an event on the owning NPC.
  - Used to make the hugbot hug extra code, specifically for the cooldown

- Changes to existing code:
  - `ComponentFilter : UtilityQueryFilter` gets `RetainWithComp` added which, as the name implies, retains entities with the specified comps rather than removing them. Basically, it lets you negate the filter.
  - `SpeakOperator : HTNOperator`'s `speech` field can use a `LocalizedDataSet` instead of just a locstring now
    - (I updated all of the existing usages for this)
  -

* two arms

* wait what if we just used mimebot arms so it doesn't look awful

* smort

24 files changed:
Content.Client/Silicons/Bots/HugBotSystem.cs [new file with mode: 0644]
Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs [new file with mode: 0644]
Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs [new file with mode: 0644]
Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs
Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs [new file with mode: 0644]
Content.Server/NPC/Queries/Queries/ComponentFilter.cs
Content.Server/NPC/Systems/NPCUtilitySystem.cs
Content.Server/Silicons/Bots/HugBotSystem.cs [new file with mode: 0644]
Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/Bots/HugBotComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/Bots/SharedHugBotSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/_Moffstation/recipes/tags.ftl [new file with mode: 0644]
Resources/Locale/en-US/npc/hugbot.ftl [new file with mode: 0644]
Resources/Prototypes/Body/Prototypes/bot.yml
Resources/Prototypes/Catalog/hugbot.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
Resources/Prototypes/NPCs/debug.yml
Resources/Prototypes/NPCs/firebot.yml
Resources/Prototypes/NPCs/hugbot.yml [new file with mode: 0644]
Resources/Prototypes/NPCs/medibot.yml
Resources/Prototypes/Recipes/Crafting/Graphs/bots/hugbot.yml [new file with mode: 0644]
Resources/Prototypes/Recipes/Crafting/bots.yml
Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/hugbot.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/meta.json [new file with mode: 0644]

diff --git a/Content.Client/Silicons/Bots/HugBotSystem.cs b/Content.Client/Silicons/Bots/HugBotSystem.cs
new file mode 100644 (file)
index 0000000..b40fe51
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Silicons.Bots;
+
+namespace Content.Client.Silicons.Bots;
+
+public sealed partial class HugBotSystem : SharedHugBotSystem;
diff --git a/Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs
new file mode 100644 (file)
index 0000000..fe76968
--- /dev/null
@@ -0,0 +1,31 @@
+using Content.Shared.Emag.Systems;
+
+namespace Content.Server.NPC.HTN.Preconditions;
+
+/// <summary>
+/// A precondition which is met if the NPC is emagged with <see cref="EmagType"/>, as computed by
+/// <see cref="EmagSystem.CheckFlag"/>. This is useful for changing NPC behavior in the case that the NPC is emagged,
+/// eg. like a helper NPC bot turning evil.
+/// </summary>
+public sealed partial class IsEmaggedPrecondition : HTNPrecondition
+{
+    private EmagSystem _emag;
+
+    /// <summary>
+    /// The type of emagging to check for.
+    /// </summary>
+    [DataField]
+    public EmagType EmagType = EmagType.Interaction;
+
+    public override void Initialize(IEntitySystemManager sysManager)
+    {
+        base.Initialize(sysManager);
+        _emag = sysManager.GetEntitySystem<EmagSystem>();
+    }
+
+    public override bool IsMet(NPCBlackboard blackboard)
+    {
+        var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
+        return _emag.CheckFlag(owner, EmagType);
+    }
+}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs
new file mode 100644 (file)
index 0000000..a1440ac
--- /dev/null
@@ -0,0 +1,70 @@
+using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Interactions;
+using Content.Shared.CombatMode;
+using Content.Shared.Weapons.Melee;
+
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Melee;
+
+/// <summary>
+/// Something between <see cref="MeleeOperator"/> and <see cref="InteractWithOperator"/>, this operator causes the NPC
+/// to attempt a SINGLE <see cref="SharedMeleeWeaponSystem.AttemptLightAttack">melee attack</see> on the specified
+/// <see cref="TargetKey">target</see>.
+/// </summary>
+public sealed partial class MeleeAttackOperator : HTNOperator
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    private SharedMeleeWeaponSystem _melee;
+
+    /// <summary>
+    /// Key that contains the target entity.
+    /// </summary>
+    [DataField(required: true)]
+    public string TargetKey = default!;
+
+    public override void Initialize(IEntitySystemManager sysManager)
+    {
+        base.Initialize(sysManager);
+        _melee = sysManager.GetEntitySystem<SharedMeleeWeaponSystem>();
+    }
+
+    public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
+    {
+        base.TaskShutdown(blackboard, status);
+
+        ExitCombatMode(blackboard);
+    }
+
+    public override void PlanShutdown(NPCBlackboard blackboard)
+    {
+        base.PlanShutdown(blackboard);
+
+        ExitCombatMode(blackboard);
+    }
+
+    public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+    {
+        var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
+
+        if (!_entManager.TryGetComponent<CombatModeComponent>(owner, out var combatMode) ||
+            !_melee.TryGetWeapon(owner, out var weaponUid, out var weapon))
+        {
+            return HTNOperatorStatus.Failed;
+        }
+
+        _entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, true, combatMode);
+
+
+        if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target, _entManager) ||
+            !_melee.AttemptLightAttack(owner, weaponUid, weapon, target))
+        {
+            return HTNOperatorStatus.Continuing;
+        }
+
+        return HTNOperatorStatus.Finished;
+    }
+
+    private void ExitCombatMode(NPCBlackboard blackboard)
+    {
+        var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
+        _entManager.System<SharedCombatModeSystem>().SetInCombatMode(owner, false);
+    }
+}
index 8a4c655a39b5d18f2d631445275b43249763c2a2..f69a0771f9fd0d9806336911c1fc41140490e284 100644 (file)
@@ -1,13 +1,21 @@
 using Content.Server.Chat.Systems;
+using Content.Shared.Dataset;
+using Content.Shared.Random.Helpers;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using static Content.Server.NPC.HTN.PrimitiveTasks.Operators.SpeakOperator.SpeakOperatorSpeech;
 
 namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
 
 public sealed partial class SpeakOperator : HTNOperator
 {
     private ChatSystem _chat = default!;
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
 
     [DataField(required: true)]
-    public string Speech = string.Empty;
+    public SpeakOperatorSpeech Speech;
 
     /// <summary>
     /// Whether to hide message from chat window and logs.
@@ -18,15 +26,51 @@ public sealed partial class SpeakOperator : HTNOperator
     public override void Initialize(IEntitySystemManager sysManager)
     {
         base.Initialize(sysManager);
-
         _chat = sysManager.GetEntitySystem<ChatSystem>();
     }
 
     public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
     {
+        LocId speechLocId;
+        switch (Speech)
+        {
+            case LocalizedSetSpeakOperatorSpeech localizedDataSet:
+                if (!_proto.TryIndex(localizedDataSet.LineSet, out var speechSet))
+                    return HTNOperatorStatus.Failed;
+                speechLocId = _random.Pick(speechSet);
+                break;
+            case SingleSpeakOperatorSpeech single:
+                speechLocId = single.Line;
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(Speech));
+        }
+
         var speaker = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
-        _chat.TrySendInGameICMessage(speaker, Loc.GetString(Speech), InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden);
+        _chat.TrySendInGameICMessage(
+            speaker,
+            Loc.GetString(speechLocId),
+            InGameICChatType.Speak,
+            hideChat: Hidden,
+            hideLog: Hidden
+        );
 
         return base.Update(blackboard, frameTime);
     }
+
+    [ImplicitDataDefinitionForInheritors, MeansImplicitUse]
+    public abstract partial class SpeakOperatorSpeech
+    {
+        public sealed partial class SingleSpeakOperatorSpeech : SpeakOperatorSpeech
+        {
+            [DataField(required: true)]
+            public string Line;
+        }
+
+        public sealed partial class LocalizedSetSpeakOperatorSpeech : SpeakOperatorSpeech
+        {
+            [DataField(required: true)]
+            public ProtoId<LocalizedDatasetPrototype> LineSet;
+        }
+    }
 }
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs
new file mode 100644 (file)
index 0000000..4bbdbb3
--- /dev/null
@@ -0,0 +1,47 @@
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;
+
+/// <summary>
+/// Raises an <see cref="HTNRaisedEvent"/> on the <see cref="NPCBlackboard.Owner">owner</see>. The event will contain
+/// the specified <see cref="Args"/>, and if not null, the value of <see cref="TargetKey"/>.
+/// </summary>
+public sealed partial class RaiseEventForOwnerOperator : HTNOperator
+{
+    [Dependency] private readonly IEntityManager _entMan = default!;
+
+    /// <summary>
+    /// The conceptual "target" of this event. Note that this is NOT the entity for which the event is raised. If null,
+    /// <see cref="HTNRaisedEvent.Target"/> will be null.
+    /// </summary>
+    [DataField]
+    public string? TargetKey;
+
+    /// <summary>
+    /// The data contained in the raised event. Since <see cref="HTNRaisedEvent"/> is itself pretty meaningless, this is
+    /// included to give some context of what the event is actually supposed to mean.
+    /// </summary>
+    [DataField(required: true)]
+    public EntityEventArgs Args;
+
+    public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+    {
+        _entMan.EventBus.RaiseLocalEvent(
+            blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
+            new HTNRaisedEvent(
+                blackboard.GetValue<EntityUid>(NPCBlackboard.Owner),
+                TargetKey is { } targetKey ? blackboard.GetValue<EntityUid>(targetKey) : null,
+                Args
+            )
+        );
+
+        return HTNOperatorStatus.Finished;
+    }
+}
+
+public sealed partial class HTNRaisedEvent(EntityUid owner, EntityUid? target, EntityEventArgs args) : EntityEventArgs
+{
+    // Owner and target are both included here in case we want to add a "RaiseEventForTargetOperator" in the future
+    // while reusing this event.
+    public EntityUid Owner = owner;
+    public EntityUid? Target = target;
+    public EntityEventArgs Args = args;
+}
index 0df4bd902f1f787acfdd0600eb7a94b18dbba8ef..5ba63bf6f765a1f67eb17c361c57e4629e7c968c 100644 (file)
@@ -9,4 +9,11 @@ public sealed partial class ComponentFilter : UtilityQueryFilter
     /// </summary>
     [DataField("components", required: true)]
     public ComponentRegistry Components = new();
+
+    /// <summary>
+    /// If true, this filter retains entities with ALL of the specified components. If false, this filter removes
+    /// entities with ANY of the specified components.
+    /// </summary>
+    [DataField]
+    public bool RetainWithComp = true;
 }
index 813626a1c44946c56a9e63201fbc5c8e69160e37..81f9415121b884add6a927a061637c3d7afcc636 100644 (file)
@@ -512,11 +512,12 @@ public sealed class NPCUtilitySystem : EntitySystem
                 {
                     foreach (var comp in compFilter.Components)
                     {
-                        if (HasComp(ent, comp.Value.Component.GetType()))
-                            continue;
-
-                        _entityList.Add(ent);
-                        break;
+                        var hasComp = HasComp(ent, comp.Value.Component.GetType());
+                        if (!compFilter.RetainWithComp == hasComp)
+                        {
+                            _entityList.Add(ent);
+                            break;
+                        }
                     }
                 }
 
diff --git a/Content.Server/Silicons/Bots/HugBotSystem.cs b/Content.Server/Silicons/Bots/HugBotSystem.cs
new file mode 100644 (file)
index 0000000..4be948f
--- /dev/null
@@ -0,0 +1,65 @@
+using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;
+using Content.Shared.Silicons.Bots;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Silicons.Bots;
+
+/// <summary>
+/// Beyond what <see cref="SharedHugBotSystem"/> does, this system manages the "lifecycle" of
+/// <see cref="RecentlyHuggedByHugBotComponent"/>.
+/// </summary>
+public sealed class HugBotSystem : SharedHugBotSystem
+{
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<HugBotComponent, HTNRaisedEvent>(OnHtnRaisedEvent);
+    }
+
+    private void OnHtnRaisedEvent(Entity<HugBotComponent> entity, ref HTNRaisedEvent args)
+    {
+        if (args.Args is not HugBotDidHugEvent ||
+            args.Target is not {} target)
+            return;
+
+        var ev = new HugBotHugEvent(GetNetEntity(entity));
+        RaiseLocalEvent(target, ev);
+
+        ApplyHugBotCooldown(entity, target);
+    }
+
+    /// <summary>
+    /// Applies <see cref="RecentlyHuggedByHugBotComponent"/> to <paramref name="target"/> based on the configuration of
+    /// <paramref name="hugBot"/>.
+    /// </summary>
+    public void ApplyHugBotCooldown(Entity<HugBotComponent> hugBot, EntityUid target)
+    {
+        var hugged = EnsureComp<RecentlyHuggedByHugBotComponent>(target);
+        hugged.CooldownCompleteAfter = _gameTiming.CurTime + hugBot.Comp.HugCooldown;
+    }
+
+    public override void Update(float frameTime)
+    {
+        // Iterate through all RecentlyHuggedByHugBot entities...
+        var huggedEntities = AllEntityQuery<RecentlyHuggedByHugBotComponent>();
+        while (huggedEntities.MoveNext(out var huggedEnt, out var huggedComp))
+        {
+            // ... and if their cooldown is complete...
+            if (huggedComp.CooldownCompleteAfter <= _gameTiming.CurTime)
+            {
+                // ... remove it, allowing them to receive the blessing of hugs once more.
+                RemCompDeferred<RecentlyHuggedByHugBotComponent>(huggedEnt);
+            }
+        }
+    }
+}
+
+/// <summary>
+/// This event is indirectly raised (by being <see cref="HTNRaisedEvent.Args"/>) on a HugBot when it hugs (or emaggedly
+/// punches) an entity.
+/// </summary>
+[Serializable, DataDefinition]
+public sealed partial class HugBotDidHugEvent : EntityEventArgs;
diff --git a/Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs b/Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs
new file mode 100644 (file)
index 0000000..c6c7d47
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Shared.Silicons.Bots;
+
+namespace Content.Server.Silicons.Bots;
+
+/// <summary>
+/// This marker component indicates that its entity has been recently hugged by a HugBot and should not be hugged again
+/// before <see cref="CooldownCompleteAfter">a cooldown period</see> in order to prevent hug spam.
+/// </summary>
+/// <see cref="SharedHugBotSystem"/>
+[RegisterComponent, AutoGenerateComponentPause]
+public sealed partial class RecentlyHuggedByHugBotComponent : Component
+{
+    [DataField, AutoPausedField]
+    public TimeSpan CooldownCompleteAfter = TimeSpan.MinValue;
+}
diff --git a/Content.Shared/Silicons/Bots/HugBotComponent.cs b/Content.Shared/Silicons/Bots/HugBotComponent.cs
new file mode 100644 (file)
index 0000000..64281b6
--- /dev/null
@@ -0,0 +1,12 @@
+namespace Content.Shared.Silicons.Bots;
+
+/// <summary>
+/// This component describes how a HugBot hugs.
+/// </summary>
+/// <see cref="SharedHugBotSystem"/>
+[RegisterComponent, AutoGenerateComponentState]
+public sealed partial class HugBotComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public TimeSpan HugCooldown = TimeSpan.FromMinutes(2);
+}
diff --git a/Content.Shared/Silicons/Bots/SharedHugBotSystem.cs b/Content.Shared/Silicons/Bots/SharedHugBotSystem.cs
new file mode 100644 (file)
index 0000000..b5dec71
--- /dev/null
@@ -0,0 +1,38 @@
+using Content.Shared.Emag.Systems;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Silicons.Bots;
+
+/// <summary>
+/// This system handles HugBots.
+/// </summary>
+public abstract class SharedHugBotSystem : EntitySystem
+{
+    [Dependency] private readonly EmagSystem _emag = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<HugBotComponent, GotEmaggedEvent>(OnEmagged);
+    }
+
+    private void OnEmagged(Entity<HugBotComponent> entity, ref GotEmaggedEvent args)
+    {
+        if (!_emag.CompareFlag(args.Type, EmagType.Interaction) ||
+            _emag.CheckFlag(entity, EmagType.Interaction) ||
+            !TryComp<HugBotComponent>(entity, out var hugBot))
+            return;
+
+        // HugBot HTN checks for emag state within its own logic, so we don't need to change anything here.
+
+        args.Handled = true;
+    }
+}
+
+/// <summary>
+/// This event is raised on an entity when it is hugged by a HugBot.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class HugBotHugEvent(NetEntity hugBot) : EntityEventArgs
+{
+    public readonly NetEntity HugBot = hugBot;
+}
diff --git a/Resources/Locale/en-US/_Moffstation/recipes/tags.ftl b/Resources/Locale/en-US/_Moffstation/recipes/tags.ftl
new file mode 100644 (file)
index 0000000..73b3925
--- /dev/null
@@ -0,0 +1 @@
+construction-graph-tag-boxhug = a box of hugs
diff --git a/Resources/Locale/en-US/npc/hugbot.ftl b/Resources/Locale/en-US/npc/hugbot.ftl
new file mode 100644 (file)
index 0000000..6b38eda
--- /dev/null
@@ -0,0 +1,26 @@
+hugbot-start-hug-1 = LEVEL 5 HUG DEFICIENCY DETECTED!
+hugbot-start-hug-2 = You look like you need a hug!
+hugbot-start-hug-3 = Aww, somebody needs a hug!
+hugbot-start-hug-4 = Target acquired; Initiating hug routine.
+hugbot-start-hug-5 = Hold still, please.
+hugbot-start-hug-6 = Hugs!
+hugbot-start-hug-7 = Deploying HUG.
+hugbot-start-hug-8 = I am designed to hug, and you WILL be hugged.
+
+hugbot-finish-hug-1 = All done.
+hugbot-finish-hug-2 = Hug routine terminated.
+hugbot-finish-hug-3 = Feel better?
+hugbot-finish-hug-4 = Feel better soon!
+hugbot-finish-hug-5 = You are loved.
+hugbot-finish-hug-6 = You matter.
+hugbot-finish-hug-7 = It always gets better!
+hugbot-finish-hug-8 = Hug: COMPLETE.
+
+hugbot-emagged-finish-hug-1 = Actually, fuck you.
+hugbot-emagged-finish-hug-2 = Nobody loves you.
+hugbot-emagged-finish-hug-3 = Ewww... no.
+hugbot-emagged-finish-hug-4 = It can only get worse from here!
+hugbot-emagged-finish-hug-5 = Fucking crybaby.
+hugbot-emagged-finish-hug-6 = Go die.
+hugbot-emagged-finish-hug-7 = Drop dead.
+hugbot-emagged-finish-hug-8 = You are alone in this universe.
index 848db2a4fd5a95a96a63623afd128fc20735f9d3..ae9bd4a77a67c17314e3c153cd8b68b459ad4424 100644 (file)
@@ -5,3 +5,19 @@
   slots:
     hand 1:
       part: LeftArmBorg
+
+# It's like a medibot or a cleanbot except it has two arms to hug :)
+- type: body
+  id: HugBot
+  name: "hugBot"
+  root: box
+  slots:
+    box:
+      part: TorsoBorg
+      connections:
+      - right_arm
+      - left_arm
+    right_arm:
+      part: RightArmBorg
+    left_arm:
+      part: LeftArmBorg
diff --git a/Resources/Prototypes/Catalog/hugbot.yml b/Resources/Prototypes/Catalog/hugbot.yml
new file mode 100644 (file)
index 0000000..469bb11
--- /dev/null
@@ -0,0 +1,17 @@
+- type: localizedDataset
+  id: HugBotStarts
+  values:
+    prefix: hugbot-start-hug-
+    count: 8
+
+- type: localizedDataset
+  id: HugBotFinishes
+  values:
+    prefix: hugbot-finish-hug-
+    count: 8
+
+- type: localizedDataset
+  id: EmaggedHugBotFinishes
+  values:
+    prefix: hugbot-emagged-finish-hug-
+    count: 8
index fd700d7a4c9d80314c5f7e7a1ae0446d6ee887d6..a538243192bbb0853492228675accc4353582c53 100644 (file)
     - Supply
   - type: Puller
     needsHands: false
+
+- type: entity
+  parent: [ MobSiliconBase, MobCombat ]
+  id: MobHugBot
+  name: hugbot
+  description: Awww, who needs a hug?
+  components:
+  - type: Sprite
+    sprite: Mobs/Silicon/Bots/hugbot.rsi
+    state: hugbot
+  - type: Construction
+    graph: HugBot
+    node: bot
+  - type: MovementSpeedModifier
+    baseWalkSpeed: 2
+    baseSprintSpeed: 3
+  - type: MeleeWeapon
+    soundHit:
+      path: /Audio/Weapons/boxingpunch1.ogg
+    angle: 30
+    animation: WeaponArcPunch
+    damage:
+      types:
+        Blunt: 2
+  - type: Anchorable
+  - type: Hands # This probably REALLY needs hand whitelisting, but we NEED hands for hugs, so...
+  - type: ComplexInteraction # Hugging is a complex interaction, apparently.
+  - type: HugBot
+  - type: Body
+    prototype: HugBot
+  - type: HTN
+    rootTask:
+      task: HugBotCompound
+  - type: InteractionPopup
+    interactSuccessString: hugging-success-generic
+    interactSuccessSound: /Audio/Effects/thudswoosh.ogg
+    messagePerceivedByOthers: hugging-success-generic-others
index c7929be10377e8a5ebeb810d6404b3e40ea7f6f2..802e6b4b57657e1b096c778fd6fda5be9f01cde3 100644 (file)
@@ -68,7 +68,7 @@
           - !type:KeyFloatLessPrecondition
             key: Count
             value: 50
-      
+
       - !type:HTNPrimitiveTask
           operator: !type:RandomOperator
             targetKey: IdleTime
@@ -85,6 +85,5 @@
     - tasks:
       - !type:HTNPrimitiveTask
         operator: !type:SpeakOperator
-          speech: "fuck!"
-
\ No newline at end of file
+          speech: !type:SingleSpeakOperatorSpeech
+            line: "fuck!"
index 2da9da50d25c0f73a265e1b5cef16ac1b8d5b4c7..b3a62280cce842c656183aa703cbd48ba1db34ce 100644 (file)
@@ -6,8 +6,8 @@
           task: DouseFireTargetCompound
     - tasks:
         - !type:HTNCompoundTask
-          task: IdleCompound    
-          
+          task: IdleCompound
+
 - type: htnCompound
   id: DouseFireTargetCompound
   branches:
@@ -18,8 +18,9 @@
 
         - !type:HTNPrimitiveTask
           operator: !type:SpeakOperator
-            speech: firebot-fire-detected
-            hidden: true  
+            speech: !type:SingleSpeakOperatorSpeech
+              line: firebot-fire-detected
+            hidden: true
 
         - !type:HTNPrimitiveTask
           operator: !type:MoveToOperator
diff --git a/Resources/Prototypes/NPCs/hugbot.yml b/Resources/Prototypes/NPCs/hugbot.yml
new file mode 100644 (file)
index 0000000..1a56b22
--- /dev/null
@@ -0,0 +1,134 @@
+- type: htnCompound
+  id: HugBotCompound
+  branches:
+  - tasks:
+    - !type:HTNCompoundTask
+      task: HugNearbyCompound
+  - tasks:
+    - !type:HTNCompoundTask
+      task: IdleCompound
+
+- type: htnCompound
+  id: HugNearbyCompound
+  branches:
+  - tasks:
+    # Locate hug recipient.
+    - !type:HTNPrimitiveTask
+      operator: !type:UtilityOperator
+        proto: NearbyNeedingHug
+
+    # Announce intent to hug
+    - !type:HTNPrimitiveTask
+      operator: !type:SpeakOperator
+        speech: !type:LocalizedSetSpeakOperatorSpeech
+          lineSet: HugBotStarts
+        hidden: true
+
+    # Approach hug recipient
+    - !type:HTNPrimitiveTask
+      operator: !type:MoveToOperator
+        pathfindInPlanning: true
+        removeKeyOnFinish: false
+        targetKey: TargetCoordinates
+        pathfindKey: TargetPathfind
+        rangeKey: MeleeRange
+
+    # HUG!!
+    - !type:HTNCompoundTask
+      task: HugBotHugCompound
+
+    # Stick around to enjoy the hug instead of just running up, squeezing and leaving.
+    - !type:HTNPrimitiveTask
+      operator: !type:SetFloatOperator
+        targetKey: IdleTime
+        amount: 1
+    - !type:HTNPrimitiveTask
+      operator: !type:WaitOperator
+        key: IdleTime
+      preconditions:
+      - !type:KeyExistsPrecondition
+        key: IdleTime
+
+    # Special case operator which applies the hugbot cooldown.
+    - !type:HTNPrimitiveTask
+      operator: !type:RaiseEventForOwnerOperator
+        args: !type:HugBotDidHugEvent
+        targetKey: Target
+
+    # Announce that the hug is completed.
+    - !type:HTNCompoundTask
+      task: HugBotFinishSpeakCompound
+
+- type: htnCompound
+  id: HugBotHugCompound
+  branches:
+  # Hit if emagged
+  - preconditions:
+    - !type:IsEmaggedPrecondition
+    tasks:
+    - !type:HTNPrimitiveTask
+      preconditions:
+      - !type:TargetInRangePrecondition
+        targetKey: Target
+        rangeKey: InteractRange
+      operator: !type:MeleeAttackOperator
+        targetKey: Target
+      services:
+      - !type:UtilityService
+        id: HugService
+        proto: NearbyNeedingHug
+        key: Target
+  # Hug otherwise
+  - tasks:
+    - !type:HTNPrimitiveTask
+      preconditions:
+      - !type:TargetInRangePrecondition
+        targetKey: Target
+        rangeKey: InteractRange
+      operator: !type:InteractWithOperator
+        targetKey: Target
+      services:
+      - !type:UtilityService
+        id: HugService
+        proto: NearbyNeedingHug
+        key: Target
+
+- type: htnCompound
+  id: HugBotFinishSpeakCompound
+  branches:
+  # Say mean things if emagged
+  - preconditions:
+    - !type:IsEmaggedPrecondition
+    tasks:
+    - !type:HTNPrimitiveTask
+      operator: !type:SpeakOperator
+        speech: !type:LocalizedSetSpeakOperatorSpeech
+          lineSet: EmaggedHugBotFinishes
+        hidden: true
+  # Say nice thing otherwise
+  - tasks:
+    - !type:HTNPrimitiveTask
+      operator: !type:SpeakOperator
+        speech: !type:LocalizedSetSpeakOperatorSpeech
+          lineSet: HugBotFinishes
+        hidden: true
+
+- type: utilityQuery
+  id: NearbyNeedingHug
+  query:
+  - !type:ComponentQuery
+    components:
+    - type: HumanoidAppearance
+      species: Human # This specific value isn't actually used, so don't worry about it being just `Human`.
+  - !type:ComponentFilter
+    retainWithComp: false
+    components:
+    - type: RecentlyHuggedByHugBot
+  considerations:
+  - !type:TargetDistanceCon
+    curve: !type:PresetCurve
+      preset: TargetDistance
+  - !type:TargetAccessibleCon
+    curve: !type:BoolCurve
+  - !type:TargetInLOSOrCurrentCon
+    curve: !type:BoolCurve
index c0853984eed1dce483a6d635ee6c282674d1b752..1cd6352e168086e8808c1f54195dc0fd98d4afd2 100644 (file)
@@ -20,7 +20,8 @@
 
         - !type:HTNPrimitiveTask
           operator: !type:SpeakOperator
-            speech: medibot-start-inject
+            speech: !type:SingleSpeakOperatorSpeech
+              line: medibot-start-inject
             hidden: true
 
         - !type:HTNPrimitiveTask
diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/bots/hugbot.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/bots/hugbot.yml
new file mode 100644 (file)
index 0000000..56ee380
--- /dev/null
@@ -0,0 +1,33 @@
+- type: constructionGraph
+  id: HugBot
+  start: start
+  graph:
+  - node: start
+    edges:
+    - to: bot
+      steps:
+      - tag: BoxHug
+        icon:
+          sprite: Objects/Storage/boxes.rsi
+          state: box_hug
+        name: construction-graph-tag-boxhug
+      - tag: ProximitySensor
+        icon:
+          sprite: Objects/Misc/proximity_sensor.rsi
+          state: icon
+        name: construction-graph-tag-proximity-sensor
+        doAfter: 2
+      - tag: BorgArm
+        icon:
+          sprite: Mobs/Silicon/drone.rsi
+          state: l_hand
+        name: construction-graph-tag-borg-arm
+        doAfter: 2
+      - tag: BorgArm
+        icon:
+          sprite: Mobs/Silicon/drone.rsi
+          state: l_hand
+        name: construction-graph-tag-borg-arm
+        doAfter: 2
+  - node: bot
+    entity: MobHugBot
index f76d545e94506d417fb527d0e35aaf6d62cf3244..d5eb1f941eadb23530247ba23e69c34bddaf4e70 100644 (file)
   targetNode: bot
   category: construction-category-utilities
   objectType: Item
+
+- type: construction
+  id: hugbot
+  graph: HugBot
+  startNode: start
+  targetNode: bot
+  category: construction-category-utilities
+  objectType: Item
diff --git a/Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/hugbot.png b/Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/hugbot.png
new file mode 100644 (file)
index 0000000..4d3dbcf
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/hugbot.png differ
diff --git a/Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/meta.json b/Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/meta.json
new file mode 100644 (file)
index 0000000..07ab403
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-4.0",
+  "copyright": "Original sprite made by compilatron (Discord) for SS13, relicensed for SS14/Moffstation; modified by Centronias (GitHub) to have two arms",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+    "states": [
+        {
+            "name": "hugbot"
+        }
+    ]
+}