Adds HugBot (#37557)
* - 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
This commit is contained in:
5
Content.Client/Silicons/Bots/HugBotSystem.cs
Normal file
5
Content.Client/Silicons/Bots/HugBotSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using Content.Shared.Silicons.Bots;
|
||||||
|
|
||||||
|
namespace Content.Client.Silicons.Bots;
|
||||||
|
|
||||||
|
public sealed partial class HugBotSystem : SharedHugBotSystem;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
using Content.Server.Chat.Systems;
|
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;
|
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||||
|
|
||||||
public sealed partial class SpeakOperator : HTNOperator
|
public sealed partial class SpeakOperator : HTNOperator
|
||||||
{
|
{
|
||||||
private ChatSystem _chat = default!;
|
private ChatSystem _chat = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
|
||||||
[DataField(required: true)]
|
[DataField(required: true)]
|
||||||
public string Speech = string.Empty;
|
public SpeakOperatorSpeech Speech;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether to hide message from chat window and logs.
|
/// 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)
|
public override void Initialize(IEntitySystemManager sysManager)
|
||||||
{
|
{
|
||||||
base.Initialize(sysManager);
|
base.Initialize(sysManager);
|
||||||
|
|
||||||
_chat = sysManager.GetEntitySystem<ChatSystem>();
|
_chat = sysManager.GetEntitySystem<ChatSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
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);
|
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);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,4 +9,11 @@ public sealed partial class ComponentFilter : UtilityQueryFilter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("components", required: true)]
|
[DataField("components", required: true)]
|
||||||
public ComponentRegistry Components = new();
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -512,13 +512,14 @@ public sealed class NPCUtilitySystem : EntitySystem
|
|||||||
{
|
{
|
||||||
foreach (var comp in compFilter.Components)
|
foreach (var comp in compFilter.Components)
|
||||||
{
|
{
|
||||||
if (HasComp(ent, comp.Value.Component.GetType()))
|
var hasComp = HasComp(ent, comp.Value.Component.GetType());
|
||||||
continue;
|
if (!compFilter.RetainWithComp == hasComp)
|
||||||
|
{
|
||||||
_entityList.Add(ent);
|
_entityList.Add(ent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var ent in _entityList)
|
foreach (var ent in _entityList)
|
||||||
{
|
{
|
||||||
|
|||||||
65
Content.Server/Silicons/Bots/HugBotSystem.cs
Normal file
65
Content.Server/Silicons/Bots/HugBotSystem.cs
Normal file
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
12
Content.Shared/Silicons/Bots/HugBotComponent.cs
Normal file
12
Content.Shared/Silicons/Bots/HugBotComponent.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
38
Content.Shared/Silicons/Bots/SharedHugBotSystem.cs
Normal file
38
Content.Shared/Silicons/Bots/SharedHugBotSystem.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
1
Resources/Locale/en-US/_Moffstation/recipes/tags.ftl
Normal file
1
Resources/Locale/en-US/_Moffstation/recipes/tags.ftl
Normal file
@@ -0,0 +1 @@
|
|||||||
|
construction-graph-tag-boxhug = a box of hugs
|
||||||
26
Resources/Locale/en-US/npc/hugbot.ftl
Normal file
26
Resources/Locale/en-US/npc/hugbot.ftl
Normal file
@@ -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.
|
||||||
@@ -5,3 +5,19 @@
|
|||||||
slots:
|
slots:
|
||||||
hand 1:
|
hand 1:
|
||||||
part: LeftArmBorg
|
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
|
||||||
|
|||||||
17
Resources/Prototypes/Catalog/hugbot.yml
Normal file
17
Resources/Prototypes/Catalog/hugbot.yml
Normal file
@@ -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
|
||||||
@@ -460,3 +460,40 @@
|
|||||||
- Supply
|
- Supply
|
||||||
- type: Puller
|
- type: Puller
|
||||||
needsHands: false
|
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
|
||||||
|
|||||||
@@ -85,6 +85,5 @@
|
|||||||
- tasks:
|
- tasks:
|
||||||
- !type:HTNPrimitiveTask
|
- !type:HTNPrimitiveTask
|
||||||
operator: !type:SpeakOperator
|
operator: !type:SpeakOperator
|
||||||
speech: "fuck!"
|
speech: !type:SingleSpeakOperatorSpeech
|
||||||
|
line: "fuck!"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
|
|
||||||
- !type:HTNPrimitiveTask
|
- !type:HTNPrimitiveTask
|
||||||
operator: !type:SpeakOperator
|
operator: !type:SpeakOperator
|
||||||
speech: firebot-fire-detected
|
speech: !type:SingleSpeakOperatorSpeech
|
||||||
|
line: firebot-fire-detected
|
||||||
hidden: true
|
hidden: true
|
||||||
|
|
||||||
- !type:HTNPrimitiveTask
|
- !type:HTNPrimitiveTask
|
||||||
|
|||||||
134
Resources/Prototypes/NPCs/hugbot.yml
Normal file
134
Resources/Prototypes/NPCs/hugbot.yml
Normal file
@@ -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
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
- !type:HTNPrimitiveTask
|
- !type:HTNPrimitiveTask
|
||||||
operator: !type:SpeakOperator
|
operator: !type:SpeakOperator
|
||||||
speech: medibot-start-inject
|
speech: !type:SingleSpeakOperatorSpeech
|
||||||
|
line: medibot-start-inject
|
||||||
hidden: true
|
hidden: true
|
||||||
|
|
||||||
- !type:HTNPrimitiveTask
|
- !type:HTNPrimitiveTask
|
||||||
|
|||||||
33
Resources/Prototypes/Recipes/Crafting/Graphs/bots/hugbot.yml
Normal file
33
Resources/Prototypes/Recipes/Crafting/Graphs/bots/hugbot.yml
Normal file
@@ -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
|
||||||
@@ -53,3 +53,11 @@
|
|||||||
targetNode: bot
|
targetNode: bot
|
||||||
category: construction-category-utilities
|
category: construction-category-utilities
|
||||||
objectType: Item
|
objectType: Item
|
||||||
|
|
||||||
|
- type: construction
|
||||||
|
id: hugbot
|
||||||
|
graph: HugBot
|
||||||
|
startNode: start
|
||||||
|
targetNode: bot
|
||||||
|
category: construction-category-utilities
|
||||||
|
objectType: Item
|
||||||
|
|||||||
BIN
Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/hugbot.png
Normal file
BIN
Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/hugbot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 342 B |
14
Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/meta.json
Normal file
14
Resources/Textures/Mobs/Silicon/Bots/hugbot.rsi/meta.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user