Skip to content

Commit

Permalink
Weldbot (#1703)
Browse files Browse the repository at this point in the history
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

The Weldbot is a medibot for IPC's, Borgs and other Silicon bots. It'll
behave pretty much as a medibot would, except it welds.
Upon destruction, it will spill fuel on the ground. Nasty stuff.

---


<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

Note: The weldbot was slow in this video, and healed only 5 units of
damage. It will heal 50 now, but I don't have an up-to-date video.

https://github.com/user-attachments/assets/11c2343b-3e7e-4536-8238-4ee28d51c045

</p>
</details>

---

# Changelog

<!--
You can add an author after the `:cl:` to change the name that appears
in the changelog (ex: `:cl: Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

:cl: Timfa
- add: Added weldbot! It'll fix IPC's, Borgs and other robots. If they
stay still for long enough...

---------

Signed-off-by: Timfa <timfalken@hotmail.com>
Co-authored-by: Remuchi <72476615+Remuchi@users.noreply.github.com>
Co-authored-by: stellar-novas <stellar_novas@riseup.net>
  • Loading branch information
3 people authored Feb 7, 2025
1 parent 359cd5b commit 20a0c5b
Show file tree
Hide file tree
Showing 18 changed files with 403 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Damage;
using Content.Shared.Emag.Components;
using Content.Shared.Interaction;
using Content.Shared.Silicons.Bots;
using Content.Shared.Tag;
using Robust.Shared.Prototypes;

namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;

public sealed partial class PickNearbyWeldableOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private EntityLookupSystem _lookup = default!;
private WeldbotSystem _weldbot = default!;
private PathfindingSystem _pathfinding = default!;
private TagSystem _tagSystem = default!;

[DataField] public string RangeKey = NPCBlackboard.WeldbotWeldRange;

/// <summary>
/// Target entity to weld
/// </summary>
[DataField(required: true)]
public string TargetKey = string.Empty;

/// <summary>
/// Target entitycoordinates to move to.
/// </summary>
[DataField(required: true)]
public string TargetMoveKey = string.Empty;

public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_lookup = sysManager.GetEntitySystem<EntityLookupSystem>();
_weldbot = sysManager.GetEntitySystem<WeldbotSystem>();
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
_tagSystem = sysManager.GetEntitySystem<TagSystem>();
}

public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
CancellationToken cancelToken)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);

if (!blackboard.TryGetValue<float>(RangeKey, out var range, _entManager) || !_entManager.TryGetComponent<WeldbotComponent>(owner, out var weldbot))
return (false, null);

var damageQuery = _entManager.GetEntityQuery<DamageableComponent>();
var emagged = _entManager.HasComponent<EmaggedComponent>(owner);

foreach (var target in _lookup.GetEntitiesInRange(owner, range))
{
if (!damageQuery.TryGetComponent(target, out var damage))
continue;

var tagPrototype = _prototypeManager.Index<TagPrototype>(WeldbotWeldOperator.SiliconTag);

if (!_entManager.TryGetComponent<TagComponent>(target, out var tagComponent) || !_tagSystem.HasTag(tagComponent, tagPrototype) || !emagged && damage.DamagePerGroup["Brute"].Value == 0)
continue;

//Needed to make sure it doesn't sometimes stop right outside it's interaction range
var pathRange = SharedInteractionSystem.InteractionRange - 1f;
var path = await _pathfinding.GetPath(owner, target, pathRange, cancelToken);

if (path.Result == PathResult.NoPath)
continue;

return (true, new Dictionary<string, object>()
{
{TargetKey, target},
{TargetMoveKey, _entManager.GetComponent<TransformComponent>(target).Coordinates},
{NPCBlackboard.PathfindKey, path},
});
}

return (false, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Emag.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Silicons.Bots;
using Content.Shared.Tag;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;

namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Specific;

public sealed partial class WeldbotWeldOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private ChatSystem _chat = default!;
private WeldbotSystem _weldbot = default!;
private SharedAudioSystem _audio = default!;
private SharedInteractionSystem _interaction = default!;
private SharedPopupSystem _popup = default!;
private DamageableSystem _damageableSystem = default!;
private TagSystem _tagSystem = default!;

public const string SiliconTag = "SiliconMob";

/// <summary>
/// Target entity to inject.
/// </summary>
[DataField(required: true)]
public string TargetKey = string.Empty;

public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
_chat = sysManager.GetEntitySystem<ChatSystem>();
_weldbot = sysManager.GetEntitySystem<WeldbotSystem>();
_audio = sysManager.GetEntitySystem<SharedAudioSystem>();
_interaction = sysManager.GetEntitySystem<SharedInteractionSystem>();
_popup = sysManager.GetEntitySystem<SharedPopupSystem>();
_damageableSystem = sysManager.GetEntitySystem<DamageableSystem>();
_tagSystem = sysManager.GetEntitySystem<TagSystem>();
}

public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
{
base.TaskShutdown(blackboard, status);
blackboard.Remove<EntityUid>(TargetKey);
}

public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
{
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);

if (!blackboard.TryGetValue<EntityUid>(TargetKey, out var target, _entMan) || _entMan.Deleted(target))
return HTNOperatorStatus.Failed;

var tagPrototype = _prototypeManager.Index<TagPrototype>(SiliconTag);

if (!_entMan.TryGetComponent<TagComponent>(target, out var tagComponent) || !_tagSystem.HasTag(tagComponent, tagPrototype)
|| !_entMan.TryGetComponent<WeldbotComponent>(owner, out var botComp)
|| !_entMan.TryGetComponent<DamageableComponent>(target, out var damage)
|| !_interaction.InRangeUnobstructed(owner, target)
|| (damage.DamagePerGroup["Brute"].Value == 0 && !_entMan.HasComponent<EmaggedComponent>(owner)))
return HTNOperatorStatus.Failed;

if (botComp.IsEmagged)
{
if (!_prototypeManager.TryIndex<DamageGroupPrototype>("Burn", out var prototype))
return HTNOperatorStatus.Failed;

_damageableSystem.TryChangeDamage(target, new DamageSpecifier(prototype, 10), true, false, damage);
}
else
{
if (!_prototypeManager.TryIndex<DamageGroupPrototype>("Brute", out var prototype))
return HTNOperatorStatus.Failed;

_damageableSystem.TryChangeDamage(target, new DamageSpecifier(prototype, -50), true, false, damage);
}

_audio.PlayPvs(botComp.WeldSound, target);

if(damage.DamagePerGroup["Brute"].Value == 0) //only say "all done if we're actually done!"
_chat.TrySendInGameICMessage(owner, Loc.GetString("weldbot-finish-weld"), InGameICChatType.Speak, hideChat: true, hideLog: true);

return HTNOperatorStatus.Finished;
}
}
2 changes: 2 additions & 0 deletions Content.Server/NPC/NPCBlackboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
{"InteractRange", SharedInteractionSystem.InteractionRange},
{"MaximumIdleTime", 7f},
{MedibotInjectRange, 4f},
{WeldbotWeldRange, 4f},
{MeleeMissChance, 0.3f},
{"MeleeRange", 1f},
{"MinimumIdleTime", 2f},
Expand Down Expand Up @@ -292,6 +293,7 @@ public string GetVisionRadiusKey(IEntityManager entMan)
public const string FollowTarget = "FollowTarget";
public const string Inventory = "Inventory";
public const string MedibotInjectRange = "MedibotInjectRange";
public const string WeldbotWeldRange = "WeldbotWeldRange";

public const string MeleeMissChance = "MeleeMissChance";

Expand Down
26 changes: 26 additions & 0 deletions Content.Shared/Silicons/Bots/WeldbotComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Robust.Shared.Audio;

namespace Content.Shared.Silicons.Bots;

/// <summary>
/// Used by the server for NPC Weldbot welding.
/// Currently no clientside prediction done, only exists in shared for emag handling.
/// </summary>
[RegisterComponent]
[Access(typeof(WeldbotSystem))]
public sealed partial class WeldbotComponent : Component
{
/// <summary>
/// Sound played after welding a patient.
/// </summary>
[DataField]
public SoundSpecifier WeldSound = new SoundPathSpecifier("/Audio/Items/welder2.ogg");

[DataField]
public SoundSpecifier EmagSparkSound = new SoundCollectionSpecifier("sparks")
{
Params = AudioParams.Default.WithVolume(8f)
};

public bool IsEmagged = false;
}
27 changes: 27 additions & 0 deletions Content.Shared/Silicons/Bots/WeldbotSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Content.Shared.Emag.Systems;
using Robust.Shared.Audio.Systems;

namespace Content.Shared.Silicons.Bots;

/// <summary>
/// Handles emagging Weldbots
/// </summary>
public sealed class WeldbotSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<WeldbotComponent, GotEmaggedEvent>(OnEmagged);
}

private void OnEmagged(EntityUid uid, WeldbotComponent comp, ref GotEmaggedEvent args)
{
_audio.PlayPredicted(comp.EmagSparkSound, uid, args.UserUid);

comp.IsEmagged = true;
args.Handled = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ petting-success-honkbot = You pet {THE($target)} on {POSS-ADJ($target)} slippery
petting-success-mimebot = You pet {THE($target)} on {POSS-ADJ($target)} cold metal head.
petting-success-cleanbot = You pet {THE($target)} on {POSS-ADJ($target)} damp metal head.
petting-success-medibot = You pet {THE($target)} on {POSS-ADJ($target)} sterile metal head.
petting-success-weldbot = You pet {THE($target)} on {POSS-ADJ($target)} stained metal head.
petting-success-firebot = You pet {THE($target)} on {POSS-ADJ($target)} warm metal head.
petting-success-generic-cyborg = You pet {THE($target)} on {POSS-ADJ($target)} metal head.
petting-success-salvage-cyborg = You pet {THE($target)} on {POSS-ADJ($target)} dirty metal head.
Expand All @@ -80,6 +81,7 @@ petting-failure-honkbot = You reach out to pet {THE($target)}, but {SUBJECT($tar
petting-failure-cleanbot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy mopping!
petting-failure-mimebot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy miming!
petting-failure-medibot = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} syringe nearly stabs your hand!
petting-failure-weldbot = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} welder nearly burns your hand!
petting-failure-firebot = You reach out to pet {THE($target)}, but {SUBJECT($target)} sprays you in the face before you can get close!
petting-failure-generic-cyborg = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy stating laws!
petting-failure-salvage-cyborg = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy drilling!
Expand Down
2 changes: 2 additions & 0 deletions Resources/Locale/en-US/npc/weldbot.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
weldbot-start-weld = Hold still, please.
weldbot-finish-weld = All done.
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
- FootstepSound
- CanPilot
- SiliconEmotes
- SiliconMob
- type: Emoting
- type: GuideHelp
guides:
Expand Down
1 change: 1 addition & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- type: Tag
tags:
- DoorBumpOpener
- SiliconMob
- type: MobState
allowedStates:
- Alive
Expand Down
54 changes: 54 additions & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/weldbot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
- type: entity
parent: MobSiliconBase
id: MobWeldbot
name: weldbot
description: No substitute for an engineer, but better than nothing.
components:
- type: Weldbot
- type: Sprite
sprite: Mobs/Silicon/Bots/weldbot.rsi
state: weldbot
- type: HTN
rootTask:
task: WeldbotCompound
- type: Construction
graph: WeldBot
node: bot
- type: NoSlip
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 110
behaviors:
- !type:TriggerBehavior
- trigger:
!type:DamageTrigger
damage: 120
behaviors:
- !type:SpillBehavior
solution: tank
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: SolutionContainerManager
solutions:
tank:
reagents:
- ReagentId: WeldingFuel
Quantity: 100
- type: SentienceTarget
flavorKind: station-event-random-sentience-flavor-mechanical
- type: Anchorable
- type: InteractionPopup
interactSuccessString: petting-success-weldbot
interactFailureString: petting-failure-weldbot
interactSuccessSound:
path: /Audio/Ambience/Objects/periodic_beep.ogg
- type: ShowHealthBars
damageContainers:
- Inorganic
- Silicon
- type: ShowHealthIcons
damageContainers:
- Inorganic
- Silicon
1 change: 1 addition & 0 deletions Resources/Prototypes/Entities/Mobs/Player/ipc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
- FootstepSound
- DoorBumpOpener
- SiliconEmotes
- SiliconMob

- type: entity
save: false
Expand Down
3 changes: 3 additions & 0 deletions Resources/Prototypes/Entities/Objects/Tools/welders.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
visible: false
shader: unshaded
map: ["enum.ToggleVisuals.Layer"]
- type: Tag
tags:
- WeldingTool
- type: GenericVisualizer
visuals:
enum.ToggleVisuals.Toggled:
Expand Down
43 changes: 43 additions & 0 deletions Resources/Prototypes/NPCs/weldbot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
- type: htnCompound
id: WeldbotCompound
branches:
- tasks:
- !type:HTNCompoundTask
task: WeldNearbyCompound
- tasks:
- !type:HTNCompoundTask
task: IdleCompound

- type: htnCompound
id: WeldNearbyCompound
branches:
- tasks:
- !type:HTNPrimitiveTask
operator: !type:PickNearbyWeldableOperator
targetKey: WeldTarget
targetMoveKey: TargetCoordinates

- !type:HTNPrimitiveTask
operator: !type:SpeakOperator
speech: weldbot-start-weld
hidden: true

- !type:HTNPrimitiveTask
operator: !type:MoveToOperator
pathfindInPlanning: false

- !type:HTNPrimitiveTask
operator: !type:SetFloatOperator
targetKey: IdleTime
amount: 3

- !type:HTNPrimitiveTask
operator: !type:WaitOperator
key: IdleTime
preconditions:
- !type:KeyExistsPrecondition
key: IdleTime

- !type:HTNPrimitiveTask
operator: !type:WeldbotWeldOperator
targetKey: WeldTarget
Loading

0 comments on commit 20a0c5b

Please sign in to comment.