diff --git a/Content.Client/Silicons/Bots/HugBotSystem.cs b/Content.Client/Silicons/Bots/HugBotSystem.cs new file mode 100644 index 0000000000..b40fe51974 --- /dev/null +++ b/Content.Client/Silicons/Bots/HugBotSystem.cs @@ -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 index 0000000000..fe76968f34 --- /dev/null +++ b/Content.Server/NPC/HTN/Preconditions/IsEmaggedPrecondition.cs @@ -0,0 +1,31 @@ +using Content.Shared.Emag.Systems; + +namespace Content.Server.NPC.HTN.Preconditions; + +/// +/// A precondition which is met if the NPC is emagged with , as computed by +/// . This is useful for changing NPC behavior in the case that the NPC is emagged, +/// eg. like a helper NPC bot turning evil. +/// +public sealed partial class IsEmaggedPrecondition : HTNPrecondition +{ + private EmagSystem _emag; + + /// + /// The type of emagging to check for. + /// + [DataField] + public EmagType EmagType = EmagType.Interaction; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _emag = sysManager.GetEntitySystem(); + } + + public override bool IsMet(NPCBlackboard blackboard) + { + var owner = blackboard.GetValue(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 index 0000000000..a1440acb2e --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Melee/MeleeAttackOperator.cs @@ -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; + +/// +/// Something between and , this operator causes the NPC +/// to attempt a SINGLE melee attack on the specified +/// target. +/// +public sealed partial class MeleeAttackOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entManager = default!; + private SharedMeleeWeaponSystem _melee; + + /// + /// Key that contains the target entity. + /// + [DataField(required: true)] + public string TargetKey = default!; + + public override void Initialize(IEntitySystemManager sysManager) + { + base.Initialize(sysManager); + _melee = sysManager.GetEntitySystem(); + } + + 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(NPCBlackboard.Owner); + + if (!_entManager.TryGetComponent(owner, out var combatMode) || + !_melee.TryGetWeapon(owner, out var weaponUid, out var weapon)) + { + return HTNOperatorStatus.Failed; + } + + _entManager.System().SetInCombatMode(owner, true, combatMode); + + + if (!blackboard.TryGetValue(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(NPCBlackboard.Owner); + _entManager.System().SetInCombatMode(owner, false); + } +} diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs index 8a4c655a39..f69a0771f9 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/SpeakOperator.cs @@ -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; /// /// 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(); } 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(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 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 index 0000000000..4bbdbb3473 --- /dev/null +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/RaiseEventForOwnerOperator.cs @@ -0,0 +1,47 @@ +namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific; + +/// +/// Raises an on the owner. The event will contain +/// the specified , and if not null, the value of . +/// +public sealed partial class RaiseEventForOwnerOperator : HTNOperator +{ + [Dependency] private readonly IEntityManager _entMan = default!; + + /// + /// The conceptual "target" of this event. Note that this is NOT the entity for which the event is raised. If null, + /// will be null. + /// + [DataField] + public string? TargetKey; + + /// + /// The data contained in the raised event. Since is itself pretty meaningless, this is + /// included to give some context of what the event is actually supposed to mean. + /// + [DataField(required: true)] + public EntityEventArgs Args; + + public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) + { + _entMan.EventBus.RaiseLocalEvent( + blackboard.GetValue(NPCBlackboard.Owner), + new HTNRaisedEvent( + blackboard.GetValue(NPCBlackboard.Owner), + TargetKey is { } targetKey ? blackboard.GetValue(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; +} diff --git a/Content.Server/NPC/Queries/Queries/ComponentFilter.cs b/Content.Server/NPC/Queries/Queries/ComponentFilter.cs index 0df4bd902f..5ba63bf6f7 100644 --- a/Content.Server/NPC/Queries/Queries/ComponentFilter.cs +++ b/Content.Server/NPC/Queries/Queries/ComponentFilter.cs @@ -9,4 +9,11 @@ public sealed partial class ComponentFilter : UtilityQueryFilter /// [DataField("components", required: true)] public ComponentRegistry Components = new(); + + /// + /// If true, this filter retains entities with ALL of the specified components. If false, this filter removes + /// entities with ANY of the specified components. + /// + [DataField] + public bool RetainWithComp = true; } diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 813626a1c4..81f9415121 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -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 index 0000000000..4be948f434 --- /dev/null +++ b/Content.Server/Silicons/Bots/HugBotSystem.cs @@ -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; + +/// +/// Beyond what does, this system manages the "lifecycle" of +/// . +/// +public sealed class HugBotSystem : SharedHugBotSystem +{ + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHtnRaisedEvent); + } + + private void OnHtnRaisedEvent(Entity 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); + } + + /// + /// Applies to based on the configuration of + /// . + /// + public void ApplyHugBotCooldown(Entity hugBot, EntityUid target) + { + var hugged = EnsureComp(target); + hugged.CooldownCompleteAfter = _gameTiming.CurTime + hugBot.Comp.HugCooldown; + } + + public override void Update(float frameTime) + { + // Iterate through all RecentlyHuggedByHugBot entities... + var huggedEntities = AllEntityQuery(); + 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(huggedEnt); + } + } + } +} + +/// +/// This event is indirectly raised (by being ) on a HugBot when it hugs (or emaggedly +/// punches) an entity. +/// +[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 index 0000000000..c6c7d47e8d --- /dev/null +++ b/Content.Server/Silicons/Bots/RecentlyHuggedByHugBotComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Silicons.Bots; + +namespace Content.Server.Silicons.Bots; + +/// +/// This marker component indicates that its entity has been recently hugged by a HugBot and should not be hugged again +/// before a cooldown period in order to prevent hug spam. +/// +/// +[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 index 0000000000..64281b6316 --- /dev/null +++ b/Content.Shared/Silicons/Bots/HugBotComponent.cs @@ -0,0 +1,12 @@ +namespace Content.Shared.Silicons.Bots; + +/// +/// This component describes how a HugBot hugs. +/// +/// +[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 index 0000000000..b5dec71f20 --- /dev/null +++ b/Content.Shared/Silicons/Bots/SharedHugBotSystem.cs @@ -0,0 +1,38 @@ +using Content.Shared.Emag.Systems; +using Robust.Shared.Serialization; + +namespace Content.Shared.Silicons.Bots; + +/// +/// This system handles HugBots. +/// +public abstract class SharedHugBotSystem : EntitySystem +{ + [Dependency] private readonly EmagSystem _emag = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnEmagged); + } + + private void OnEmagged(Entity entity, ref GotEmaggedEvent args) + { + if (!_emag.CompareFlag(args.Type, EmagType.Interaction) || + _emag.CheckFlag(entity, EmagType.Interaction) || + !TryComp(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; + } +} + +/// +/// This event is raised on an entity when it is hugged by a HugBot. +/// +[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 index 0000000000..73b392534a --- /dev/null +++ b/Resources/Locale/en-US/_Moffstation/recipes/tags.ftl @@ -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 index 0000000000..6b38eda30e --- /dev/null +++ b/Resources/Locale/en-US/npc/hugbot.ftl @@ -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. diff --git a/Resources/Prototypes/Body/Prototypes/bot.yml b/Resources/Prototypes/Body/Prototypes/bot.yml index 848db2a4fd..ae9bd4a77a 100644 --- a/Resources/Prototypes/Body/Prototypes/bot.yml +++ b/Resources/Prototypes/Body/Prototypes/bot.yml @@ -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 index 0000000000..469bb1196d --- /dev/null +++ b/Resources/Prototypes/Catalog/hugbot.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index fd700d7a4c..a538243192 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -460,3 +460,40 @@ - 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 diff --git a/Resources/Prototypes/NPCs/debug.yml b/Resources/Prototypes/NPCs/debug.yml index c7929be103..802e6b4b57 100644 --- a/Resources/Prototypes/NPCs/debug.yml +++ b/Resources/Prototypes/NPCs/debug.yml @@ -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!" diff --git a/Resources/Prototypes/NPCs/firebot.yml b/Resources/Prototypes/NPCs/firebot.yml index 2da9da50d2..b3a62280cc 100644 --- a/Resources/Prototypes/NPCs/firebot.yml +++ b/Resources/Prototypes/NPCs/firebot.yml @@ -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 index 0000000000..1a56b22b92 --- /dev/null +++ b/Resources/Prototypes/NPCs/hugbot.yml @@ -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 diff --git a/Resources/Prototypes/NPCs/medibot.yml b/Resources/Prototypes/NPCs/medibot.yml index c0853984ee..1cd6352e16 100644 --- a/Resources/Prototypes/NPCs/medibot.yml +++ b/Resources/Prototypes/NPCs/medibot.yml @@ -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 index 0000000000..56ee3803e0 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/bots/hugbot.yml @@ -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 diff --git a/Resources/Prototypes/Recipes/Crafting/bots.yml b/Resources/Prototypes/Recipes/Crafting/bots.yml index f76d545e94..d5eb1f941e 100644 --- a/Resources/Prototypes/Recipes/Crafting/bots.yml +++ b/Resources/Prototypes/Recipes/Crafting/bots.yml @@ -53,3 +53,11 @@ 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 index 0000000000..4d3dbcfa07 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 index 0000000000..07ab40378d --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/meta.json @@ -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" + } + ] +}