Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plantbot #1831

Merged
merged 4 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Botany.Components;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Emag.Components;
using Content.Shared.Interaction;
using Content.Shared.Silicons.Bots;
using Robust.Shared.Prototypes;

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

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

[DataField] public string RangeKey = NPCBlackboard.PlantbotServiceRange;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate lines, add a summary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(specifically the datafield)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in e0e7f87


/// <summary>
/// Target entity to service
/// </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>();
_pathfinding = sysManager.GetEntitySystem<PathfindingSystem>();
}

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<PlantbotComponent>(owner, out _))
return (false, null);

var entityQuery = _entManager.GetEntityQuery<PlantHolderComponent>();
var emagged = _entManager.HasComponent<EmaggedComponent>(owner);

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

if(plantHolderComponent is { WaterLevel: > 80f, WeedLevel: < 1f } && (!emagged || plantHolderComponent.Dead || plantHolderComponent.WaterLevel <= 0f))
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,90 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Server.Chat.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
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 PlantbotServiceOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private ChatSystem _chat = default!;
private SharedAudioSystem _audio = default!;
private SharedInteractionSystem _interaction = default!;
private SharedPopupSystem _popup = default!;
private PlantHolderSystem _plantHolderSystem = 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>();
_audio = sysManager.GetEntitySystem<SharedAudioSystem>();
_interaction = sysManager.GetEntitySystem<SharedInteractionSystem>();
_popup = sysManager.GetEntitySystem<SharedPopupSystem>();
_plantHolderSystem = sysManager.GetEntitySystem<PlantHolderSystem>();
}

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;

if (!_entMan.TryGetComponent<PlantbotComponent>(owner, out var botComp)
|| !_entMan.TryGetComponent<PlantHolderComponent>(target, out var plantHolderComponent)
|| !_interaction.InRangeUnobstructed(owner, target)
|| (plantHolderComponent is { WaterLevel: > 80f, WeedLevel: < 1f } && (!_entMan.HasComponent<EmaggedComponent>(owner) || plantHolderComponent.Dead || plantHolderComponent.WaterLevel <= 0f)))
return HTNOperatorStatus.Failed;

if (botComp.IsEmagged)
{
_plantHolderSystem.AdjustWater(target, -10f);
_audio.PlayPvs(botComp.RemoveWaterSound, target);
}
else
{
if (plantHolderComponent.WaterLevel < 80f)
{
_plantHolderSystem.AdjustWater(target, 10);
_audio.PlayPvs(botComp.WaterSound, target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("plantbot-add-water"), InGameICChatType.Speak, hideChat: true, hideLog: true);
}
else if (plantHolderComponent.WeedLevel > 1)
{
plantHolderComponent.WeedLevel -= 1;
_audio.PlayPvs(botComp.WeedSound, target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("plantbot-remove-weeds"), InGameICChatType.Speak, hideChat: true, hideLog: true);
}
else
return HTNOperatorStatus.Failed;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use constants to make this easily readable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in e0e7f87


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 @@ -27,6 +27,7 @@ public sealed partial class NPCBlackboard : IEnumerable<KeyValuePair<string, obj
{"MaximumIdleTime", 7f},
{MedibotInjectRange, 4f},
{WeldbotWeldRange, 4f},
{PlantbotServiceRange, 4f},
{MeleeMissChance, 0.3f},
{"MeleeRange", 1f},
{"MinimumIdleTime", 2f},
Expand Down Expand Up @@ -294,6 +295,7 @@ public string GetVisionRadiusKey(IEntityManager entMan)
public const string Inventory = "Inventory";
public const string MedibotInjectRange = "MedibotInjectRange";
public const string WeldbotWeldRange = "WeldbotWeldRange";
public const string PlantbotServiceRange = "PlantbotServiceRange";

public const string MeleeMissChance = "MeleeMissChance";

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

namespace Content.Shared.Silicons.Bots;

/// <summary>
/// Used by the server for NPC Plantbot servicing.
/// Currently no clientside prediction done, only exists in shared for emag handling.
/// </summary>
[RegisterComponent]
[Access(typeof(PlantbotSystem))]
public sealed partial class PlantbotComponent : Component
{
/// <summary>
/// Sound played after watering a plantHolder.
/// </summary>
[DataField]
public SoundSpecifier WaterSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");

/// <summary>
/// Sound played after weeding a plantHolder.
/// </summary>
[DataField]
public SoundSpecifier WeedSound = new SoundPathSpecifier("/Audio/Effects/plant_rustle.ogg");

/// <summary>
/// Sound played after draining a plantHolder.
/// </summary>
[DataField]
public SoundSpecifier RemoveWaterSound = new SoundPathSpecifier("/Audio/Items/drink.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/PlantbotSystem.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 Plantbots
/// </summary>
public sealed class PlantbotSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;

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

SubscribeLocalEvent<PlantbotComponent, GotEmaggedEvent>(OnEmagged);
}

private void OnEmagged(EntityUid uid, PlantbotComponent 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 @@ -60,6 +60,7 @@ petting-failure-shadow = You're trying to pet {THE($target)}, but your hand pass
petting-success-honkbot = You pet {THE($target)} on {POSS-ADJ($target)} slippery metal head.
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-plantbot = You pet {THE($target)} on {POSS-ADJ($target)} muddy 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.
Expand All @@ -79,6 +80,7 @@ petting-success-disablerbot = You pet {THE($target)} on {POSS-ADJ($target)} prot

petting-failure-honkbot = You reach out to pet {THE($target)}, but {SUBJECT($target)} honks in refusal!
petting-failure-cleanbot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy mopping!
petting-failure-plantbot = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} clippers nearly snip your fingers off!
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!
Expand Down
2 changes: 2 additions & 0 deletions Resources/Locale/en-US/npc/plantbot.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
plantbot-add-water = Adding water.
plantbot-remove-weeds = Removing weeds.
14 changes: 14 additions & 0 deletions Resources/Prototypes/Entities/Markers/Spawners/bots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,17 @@
- type: ConditionalSpawner
prototypes:
- MobBatonBot

- type: entity
name: plantbot spawner
id: SpawnMobPlantbot
parent: MarkerBase
components:
- type: Sprite
layers:
- state: green
- sprite: Mobs/Silicon/Bots/plantbot.rsi
state: plantbot
- type: ConditionalSpawner
prototypes:
- MobPlantbot
54 changes: 54 additions & 0 deletions Resources/Prototypes/Entities/Mobs/NPCs/plantbot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
- type: entity
parent: MobSiliconBase
id: MobPlantbot
name: plantbot
description: A botanist's best friend!.
components:
- type: Plantbot
- type: Sprite
sprite: Mobs/Silicon/Bots/plantbot.rsi
state: plantbot
- type: HTN
rootTask:
task: PlantbotCompound
- type: Construction
graph: PlantBot
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: Water
Quantity: 100
- type: SentienceTarget
flavorKind: station-event-random-sentience-flavor-mechanical
- type: Anchorable
- type: InteractionPopup
interactSuccessString: petting-success-plantbot
interactFailureString: petting-failure-plantbot
interactSuccessSound:
path: /Audio/Ambience/Objects/periodic_beep.ogg
- type: ShowHealthBars
damageContainers:
- Inorganic
- Silicon
- type: ShowHealthIcons
damageContainers:
- Inorganic
- Silicon
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- type: Tag
tags:
- PlantSampleTaker
- HydroponicsToolClippers
- type: Sprite
sprite: Objects/Tools/Hydroponics/clippers.rsi
state: icon
Expand Down
38 changes: 38 additions & 0 deletions Resources/Prototypes/NPCs/plantbot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
- type: htnCompound
id: PlantbotCompound
branches:
- tasks:
- !type:HTNCompoundTask
task: ServiceNearbyPlantsCompound
- tasks:
- !type:HTNCompoundTask
task: IdleCompound

- type: htnCompound
id: ServiceNearbyPlantsCompound
branches:
- tasks:
- !type:HTNPrimitiveTask
operator: !type:PickNearbyServicableHydroponicsTrayOperator
targetKey: PlantTarget
targetMoveKey: TargetCoordinates

- !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:PlantbotServiceOperator
targetKey: PlantTarget
Loading
Loading