Add nodecrawling, implement ventcrawl with it (#5830)

* node crawling movement

* more node crawling

* quality assurance

* do-after it

* i'm ratted king

* we don't need to serialize node stuff

* rename and ser/de fix

* review fixes
This commit is contained in:
pathetic meowmeow 2026-06-28 20:54:08 -04:00 committed by GitHub
parent 495107e5e0
commit c7c6f1a67a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 800 additions and 13 deletions

View File

@ -32,6 +32,21 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
}
}
// Begin DeltaV - node crawling
private Type[] _types = new Type[] { };
[ViewVariables]
public Type[] Types
{
get => _types;
set
{
_types = value;
UpdateAll();
}
}
// End DeltaV - node crawling
public override void Initialize()
{
base.Initialize();
@ -65,6 +80,17 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
var revealed = !covered || ShowAll || scannerRevealed;
// Begin DeltaV - node crawling
foreach (var type in _types)
{
if (!HasComp(uid, type))
continue;
revealed = true;
break;
}
// End DeltaV - node crawling
// set visibility & color of each layer
foreach (var layer in args.Sprite.AllLayers)
{

View File

@ -0,0 +1,43 @@
using Content.Client.SubFloor;
using Content.Shared._DV.NodeCrawl;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client._DV.NodeCrawl;
public sealed class NodeCrawlSystem : SharedNodeCrawlSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SubFloorHideSystem _subfloor = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NodeCrawlerComponent, LocalPlayerAttachedEvent>(OnAttached);
SubscribeLocalEvent<NodeCrawlerComponent, LocalPlayerDetachedEvent>(OnDetached);
SubscribeLocalEvent<NodeCrawlerComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAttached(Entity<NodeCrawlerComponent> ent, ref LocalPlayerAttachedEvent args)
{
if (ent.Comp.Mover is not null)
_subfloor.Types = ent.Comp.RevealedComponents;
}
private void OnDetached(Entity<NodeCrawlerComponent> ent, ref LocalPlayerDetachedEvent args)
{
_subfloor.Types = new Type[] { };
}
private void OnAfterAutoHandleState(Entity<NodeCrawlerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_player.LocalEntity != ent)
return;
if (ent.Comp.Mover is not null)
_subfloor.Types = ent.Comp.RevealedComponents;
else
_subfloor.Types = new Type[] { };
}
}

View File

@ -0,0 +1,101 @@
using System.Numerics;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared._DV.NodeCrawl;
using Content.Shared.Atmos;
using Content.Shared.NodeContainer;
namespace Content.Server._DV.NodeCrawl;
public sealed class NodeCrawlSystem : SharedNodeCrawlSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrawlableNodeComponent, NodeGroupsRebuilt>(OnNodeGroupsRebuilt);
SubscribeLocalEvent<NodeCrawlerComponent, InhaleLocationEvent>(OnInhaleLocation);
SubscribeLocalEvent<NodeCrawlerComponent, ExhaleLocationEvent>(OnExhaleLocation);
SubscribeLocalEvent<NodeCrawlerComponent, AtmosExposedGetAirEvent>(OnGetAir);
}
private Entity<NodeContainerComponent>? GetNodeContainer(Entity<NodeCrawlerComponent> crawler)
{
if (!TryComp<NodeCrawlerMovementComponent>(crawler.Comp.Mover, out var mover) || mover.Node is not { } node)
return null;
if (!TryComp<NodeContainerComponent>(node, out var nodeContainer))
return null;
return (node, nodeContainer);
}
private GasMixture? GetAir(Entity<NodeCrawlerComponent> crawler)
{
if (GetNodeContainer(crawler) is not { } nodeContainer)
return null;
foreach (var containedNode in nodeContainer.Comp.Nodes.Values)
{
if (containedNode is not PipeNode pipe)
continue;
return pipe.Air;
}
return null;
}
private void OnInhaleLocation(Entity<NodeCrawlerComponent> ent, ref InhaleLocationEvent args)
{
if (GetAir(ent) is not { } air)
return;
args.Gas = air;
}
private void OnExhaleLocation(Entity<NodeCrawlerComponent> ent, ref ExhaleLocationEvent args)
{
if (GetAir(ent) is not { } air)
return;
args.Gas = air;
}
private void OnGetAir(Entity<NodeCrawlerComponent> ent, ref AtmosExposedGetAirEvent args)
{
if (args.Handled || GetAir(ent) is not { } air)
return;
args.Gas = air;
args.Handled = true;
}
private void OnNodeGroupsRebuilt(Entity<CrawlableNodeComponent> ent, ref NodeGroupsRebuilt args)
{
if (!TryComp<NodeContainerComponent>(ent, out var nodeContainer))
return;
ent.Comp.DeadEnd = false;
var set = new HashSet<EntityUid>();
foreach (var node in nodeContainer.Nodes.Values)
{
foreach (var reachable in node.ReachableNodes)
{
set.Add(reachable.Owner);
}
if (node is PipeNode pipeNode &&
node.ReachableNodes.Count != BitOperations.PopCount((uint)pipeNode.CurrentPipeDirection))
{
ent.Comp.DeadEnd = true;
}
}
ent.Comp.ReachableNodes = set;
Dirty(ent);
}
}

View File

@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared._DV.Movement; // DeltaV
using Content.Shared._DV.NodeCrawl; // DeltaV - Node Crawling
using Content.Shared.ActionBlocker;
using Content.Shared.CCVar;
using Content.Shared.Friction;
@ -50,6 +51,7 @@ public abstract partial class SharedMoverController : VirtualController
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly TileMovementSystem _tileMovement = default!; // DeltaV
[Dependency] private readonly NodeCrawlerMovementSystem _nodeCrawlerMovement = default!; // DeltaV - node crawling
protected EntityQuery<CanMoveInAirComponent> CanMoveInAirQuery;
protected EntityQuery<FootstepModifierComponent> FootstepModifierQuery;
@ -264,6 +266,11 @@ public abstract partial class SharedMoverController : VirtualController
return;
// End DeltaV Additions
// Begin DeltaV Additions - node crawling
if (_nodeCrawlerMovement.TryTick((uid, mover, physicsComponent, xform)))
return;
// End DeltaV Additions
var touching = false;
// Whether we use tilefriction or not
if (weightless || inAirHelpless)

View File

@ -65,8 +65,8 @@ public enum CollisionGroup
TabletopMachineLayer = Opaque | BulletImpassable,
// Airlocks, windoors, firelocks
GlassAirlockLayer = HighImpassable | MidImpassable | BulletImpassable | InteractImpassable,
AirlockLayer = Opaque | GlassAirlockLayer,
GlassAirlockLayer = HighImpassable | MidImpassable | LowImpassable | BulletImpassable | InteractImpassable, // DeltaV - we have ventcrawling
AirlockLayer = Opaque | LowImpassable | GlassAirlockLayer, // DeltaV - we have ventcrawling
// Airlock assembly
HumanoidBlockLayer = HighImpassable | MidImpassable,

View File

@ -101,6 +101,12 @@ public sealed partial class RatKingComponent : Component
{ RatKingOrderType.CheeseEm, "RatKingCommandCheeseEm" },
{ RatKingOrderType.Loose, "RatKingCommandLoose" }
};
/// <summary>
/// DeltaV - Servants within this radius will join the Rat King when vent crawling
/// </summary>
[DataField]
public float VentCrawlRecruitRadius = 4f;
}
[Serializable, NetSerializable]

View File

@ -0,0 +1,29 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Represents an entity in a node network that can be crawled in
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedNodeCrawlSystem), typeof(NodeCrawlerMovementSystem))]
public sealed partial class CrawlableNodeComponent : Component
{
/// <summary>
/// Other entities with <see cref="CrawlableNodeComponent" /> that can be reached from this one
/// </summary>
[ViewVariables, AutoNetworkedField]
public HashSet<EntityUid> ReachableNodes = new();
/// <summary>
/// Whether this node has an unconnected node and should be exited from on movement
/// </summary>
[ViewVariables, AutoNetworkedField]
public bool DeadEnd;
/// <summary>
/// All entities with <see cref="NodeCrawlerMovementComponent" /> that are associated with this one
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<EntityUid> Crawlers = new();
}

View File

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Marker component to use on atmos-related devices that node crawlers can enter/exit out of.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class NodeCrawlVentAccessComponent : Component;

View File

@ -0,0 +1,44 @@
using Content.Shared.DoAfter;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Handles entities that can enter and exit node-constrained movement.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(SharedNodeCrawlSystem))]
public sealed partial class NodeCrawlerComponent : Component
{
/// <summary>
/// The mover this crawler is currently being carried by, if any
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Mover;
/// <summary>
/// Components of entities to reveal while inside a mover
/// </summary>
[DataField(readOnly: true)]
public Type[] RevealedComponents;
/// <summary>
/// Whitelist for entities that will be considered as exit nodes.
/// </summary>
[DataField]
public EntityWhitelist? ExitNodes;
/// <summary>
/// How long it takes to enter a node.
/// </summary>
[DataField]
public TimeSpan EnterDelay = TimeSpan.FromSeconds(1);
}
[Serializable, NetSerializable]
public sealed partial class NodeCrawlEnterDoAfterEvent : SimpleDoAfterEvent;
[ByRefEvent]
public readonly record struct NodeCrawlerStartedCrawlingEvent(Entity<NodeCrawlerMovementComponent> Mover);

View File

@ -0,0 +1,42 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Handles node-confined movement for an entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(NodeCrawlerMovementSystem))]
public sealed partial class NodeCrawlerMovementComponent : Component
{
/// <summary>
/// The current node.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Node;
/// <summary>
/// The target node being moved to.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? TargetNode;
/// <summary>
/// The crawler being carried by this entity.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? HeldCrawler;
/// <summary>
/// The required angle to be within for deciding which node to move from the target direction
/// </summary>
[DataField]
public double RequiredAngle = Math.PI / 4f;
}
/// <summary>
/// Event raised when a node crawler arrives at a node entity.
/// </summary>
/// <param name="Node">The arrived-at node.</param>
[ByRefEvent]
public readonly record struct NodeCrawlerArrivedAtNodeEvent(EntityUid Node);

View File

@ -0,0 +1,195 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Content.Shared._DV.NodeCrawl;
public sealed class NodeCrawlerMovementSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedNodeCrawlSystem _nodeCrawl = default!;
public bool TryTick(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent> sharedMover)
{
if (!TryComp<NodeCrawlerMovementComponent>(sharedMover, out var crawler) || crawler.Node is null)
return false;
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover = (sharedMover.Owner, sharedMover.Comp1, sharedMover.Comp2, sharedMover.Comp3, crawler);
if (mover.Comp4.TargetNode is { } target)
OngoingMovement(mover, target);
else
StartMovement(mover);
return true;
}
private void StartMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover)
{
if (GetDestination(mover, mover.Comp1.HeldMoveButtons) is not { } target)
{
if (mover.Comp4.Node is not { } node)
return;
var nodeComp = Comp<CrawlableNodeComponent>(node);
if (!nodeComp.DeadEnd)
return;
if (mover.Comp4.HeldCrawler is not { } crawler)
return;
_nodeCrawl.ExitNodeCrawl((crawler, Comp<NodeCrawlerComponent>(crawler)));
return;
}
mover.Comp4.TargetNode = target;
Dirty(mover, mover.Comp4);
OngoingMovement(mover, target);
}
private void StopMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover)
{
_physics.SetLinearVelocity(mover, Vector2.Zero, body: mover.Comp2);
_physics.SetAngularVelocity(mover, 0, body: mover.Comp2);
}
private void OngoingMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover,
EntityUid target)
{
var speed = MoveSpeed(mover);
if (ReachedDestination(mover, target, speed))
{
StopMovement(mover);
SetNode((mover, mover), target);
mover.Comp4.TargetNode = null;
Dirty(mover, mover.Comp4);
if (TryComp<MovementRelayTargetComponent>(mover, out var movementTarget))
{
var evt = new NodeCrawlerArrivedAtNodeEvent(target);
RaiseLocalEvent(movementTarget.Source, ref evt);
}
StartMovement(mover);
return;
}
UpdateMovement(mover, target, speed);
}
private float MoveSpeed(Entity<InputMoverComponent> mover)
{
var moveSpeed = CompOrNull<MovementSpeedModifierComponent>(mover);
var walkSpeed = moveSpeed?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed;
var sprintSpeed = moveSpeed?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
return mover.Comp.Sprinting ? sprintSpeed : walkSpeed;
}
private void UpdateMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover,
EntityUid target,
float speed)
{
var delta = _transform.GetWorldPosition(target) - _transform.GetWorldPosition(mover.Comp3);
var facing = Angle.FromWorldVec(delta);
_transform.SetWorldRotation(mover.Comp3, facing);
var velocity = delta;
velocity.Normalize();
velocity *= speed;
_physics.SetCanCollide(mover, false);
_physics.SetLinearVelocity(mover, velocity, body: mover.Comp2);
_physics.SetAngularVelocity(mover, 0, body: mover.Comp2);
}
private bool ReachedDestination(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover,
EntityUid target,
float speed)
{
var delta = _transform.GetWorldPosition(mover.Comp3) - _transform.GetWorldPosition(target);
return delta.EqualsApprox(Vector2.Zero, speed * 0.01f);
}
private EntityUid? GetDestination(Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> ent, MoveButtons buttons)
{
if ((buttons & MoveButtons.AnyDirection) == 0)
return null;
var target = _mover.DirVecForButtons(buttons);
target = _mover.GetParentGridAngle(ent.Comp1).RotateVec(target);
if (ent.Comp4.Node is not { } node || !Exists(node) || !TryComp<CrawlableNodeComponent>(node, out var nodeCrawl))
return null;
var nodeXform = Transform(node);
var nodeWorld = _transform.GetWorldPosition(nodeXform);
var smallestTarget = EntityUid.Invalid;
var largestDot = 0d;
foreach (var reachable in nodeCrawl.ReachableNodes)
{
var reachableXform = Transform(reachable);
var reachableWorld = _transform.GetWorldPosition(reachableXform);
var delta = reachableWorld - nodeWorld;
delta.Normalize();
var deltaTargetDot = Vector2.Dot(delta, target);
if (deltaTargetDot < largestDot)
continue;
smallestTarget = reachable;
largestDot = deltaTargetDot;
}
if (!smallestTarget.Valid || largestDot <= Math.Cos(ent.Comp4.RequiredAngle))
return null;
return smallestTarget;
}
public void SetNode(Entity<NodeCrawlerMovementComponent> ent, EntityUid? node)
{
if (ent.Comp.Node == node)
return;
if (ent.Comp.Node is { } oldNode)
{
var oldNodeComp = Comp<CrawlableNodeComponent>(oldNode);
oldNodeComp.Crawlers.Remove(ent);
Dirty(oldNode, oldNodeComp);
}
if (node is { } newNode)
{
var newNodeComp = Comp<CrawlableNodeComponent>(newNode);
newNodeComp.Crawlers.Add(ent);
Dirty(newNode, newNodeComp);
}
ent.Comp.Node = node;
Dirty(ent);
}
public void SetHeldCrawler(Entity<NodeCrawlerMovementComponent> ent, EntityUid? held)
{
if (ent.Comp.HeldCrawler == held)
return;
ent.Comp.HeldCrawler = held;
Dirty(ent);
}
}

View File

@ -0,0 +1,229 @@
using Content.Shared.DoAfter;
using Content.Shared.Eye;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.RatKing;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Manages entry & exit of node crawlers into node networks
/// </summary>
public abstract class SharedNodeCrawlSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly NodeCrawlerMovementSystem _nodeCrawler = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
private const string MoverContainer = "mover-container";
private static readonly EntProtoId MoverProto = "DVNodeCrawlMover";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrawlableNodeComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
SubscribeLocalEvent<NodeCrawlerComponent, NodeCrawlEnterDoAfterEvent>(OnNodeCrawlEntryDoAfter);
SubscribeLocalEvent<NodeCrawlerComponent, NodeCrawlerArrivedAtNodeEvent>(OnArrivedAtNode);
SubscribeLocalEvent<NodeCrawlerComponent, GetVisMaskEvent>(OnGetVisMask);
SubscribeLocalEvent<CrawlableNodeComponent, ComponentShutdown>(OnCrawlableShutdown);
SubscribeLocalEvent<NodeCrawlerMovementComponent, ComponentShutdown>(OnMovementShutdown);
SubscribeLocalEvent<NodeCrawlerComponent, ComponentShutdown>(OnCrawlerShutdown);
SubscribeLocalEvent<CrawlableNodeComponent, AnchorStateChangedEvent>(OnCrawlableAnchorChanged);
SubscribeLocalEvent<RatKingComponent, NodeCrawlerStartedCrawlingEvent>(OnRatKingStartedCrawling);
}
private void OnGetVerbs(Entity<CrawlableNodeComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
var user = args.User;
var target = args.Target;
if (!TryComp<NodeCrawlerComponent>(user, out var nodeCrawler))
return;
if (!_entityWhitelist.IsWhitelistPass(nodeCrawler.ExitNodes, target))
return;
if (!args.CanAccess)
return;
args.Verbs.Add(new AlternativeVerb
{
Act = () => StartEntryDoAfter((user, nodeCrawler), target),
Text = Loc.GetString("node-crawl-enter", ("target", target)),
});
}
private void StartEntryDoAfter(Entity<NodeCrawlerComponent> ent, EntityUid target)
{
var doAfterArgs = new DoAfterArgs(EntityManager, ent.Owner, ent.Comp.EnterDelay, new NodeCrawlEnterDoAfterEvent(), ent.Owner, target)
{
BreakOnMove = true,
BreakOnDamage = true,
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
private void OnNodeCrawlEntryDoAfter(Entity<NodeCrawlerComponent> ent, ref NodeCrawlEnterDoAfterEvent args)
{
if (args.Cancelled || args.Target is not { } target)
return;
NodeCrawl(ent, target);
}
private void NodeCrawl(Entity<NodeCrawlerComponent> ent, EntityUid target)
{
if (!_net.IsServer)
return;
var mover = Spawn(MoverProto, Transform(target).Coordinates);
var crawler = Comp<NodeCrawlerMovementComponent>(mover);
var container = _container.GetContainer(mover, MoverContainer);
_container.Insert(ent.Owner, container);
ent.Comp.Mover = mover;
Dirty(ent);
var evt = new NodeCrawlerStartedCrawlingEvent((mover, crawler));
RaiseLocalEvent(ent, ref evt);
_nodeCrawler.SetNode((mover, crawler), target);
_nodeCrawler.SetHeldCrawler((mover, crawler), ent);
_mover.SetRelay(ent, mover);
_physics.SetCanCollide(ent.Owner, false);
_physics.SetCanCollide(mover, false);
_eye.RefreshVisibilityMask(ent.Owner);
}
/// <summary>
/// Causes this node crawler to exit its node crawl.
/// </summary>
/// <param name="ent">The crawler to exit node-crawl from.</param>
public void ExitNodeCrawl(Entity<NodeCrawlerComponent> ent)
{
if (ent.Comp.Mover is not { } mover)
return;
ent.Comp.Mover = null;
Dirty(ent);
var container = _container.GetContainer(mover, MoverContainer);
_container.Remove(ent.Owner, container);
foreach (var other in _container.EmptyContainer(container))
{
if (!TryComp<NodeCrawlerComponent>(other, out var otherCrawler))
continue;
otherCrawler.Mover = null;
Dirty(other, otherCrawler);
}
RemComp<RelayInputMoverComponent>(ent);
if (_net.IsServer && !TerminatingOrDeleted(mover))
QueueDel(mover); // deletion isn't predicted because client queued deletion doesn't interact well with container stuff
_physics.SetCanCollide(ent.Owner, true);
_eye.RefreshVisibilityMask(ent.Owner);
}
private void OnArrivedAtNode(Entity<NodeCrawlerComponent> ent, ref NodeCrawlerArrivedAtNodeEvent args)
{
if (!_entityWhitelist.IsWhitelistPass(ent.Comp.ExitNodes, args.Node))
return;
ExitNodeCrawl(ent);
}
private void OnGetVisMask(Entity<NodeCrawlerComponent> ent, ref GetVisMaskEvent args)
{
if (ent.Comp.Mover is null)
return;
args.VisibilityMask |= (int)VisibilityFlags.Subfloor;
}
private void OnCrawlableShutdown(Entity<CrawlableNodeComponent> ent, ref ComponentShutdown args)
{
foreach (var crawler in ent.Comp.Crawlers)
{
var movement = Comp<NodeCrawlerMovementComponent>(crawler);
if (movement.HeldCrawler is not { } held)
continue;
_nodeCrawler.SetNode((crawler, movement), null);
ExitNodeCrawl((held, Comp<NodeCrawlerComponent>(held)));
}
}
private void OnMovementShutdown(Entity<NodeCrawlerMovementComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.Node is { } node)
{
var nodeComp = Comp<CrawlableNodeComponent>(node);
nodeComp.Crawlers.Remove(ent);
Dirty(node, nodeComp);
}
if (ent.Comp.HeldCrawler is { } crawler)
{
ExitNodeCrawl((crawler, Comp<NodeCrawlerComponent>(crawler)));
}
}
private void OnCrawlerShutdown(Entity<NodeCrawlerComponent> ent, ref ComponentShutdown args)
{
ExitNodeCrawl(ent);
}
private void OnCrawlableAnchorChanged(Entity<CrawlableNodeComponent> ent, ref AnchorStateChangedEvent args)
{
if (args.Anchored)
return;
foreach (var crawler in ent.Comp.Crawlers)
{
var movement = Comp<NodeCrawlerMovementComponent>(crawler);
if (movement.HeldCrawler is not { } held)
continue;
ExitNodeCrawl((held, Comp<NodeCrawlerComponent>(held)));
}
}
private void OnRatKingStartedCrawling(Entity<RatKingComponent> ent, ref NodeCrawlerStartedCrawlingEvent args)
{
var entities = new HashSet<Entity<RatKingServantComponent>>();
_entityLookup.GetEntitiesInRange(Transform(ent).Coordinates,
ent.Comp.VentCrawlRecruitRadius,
entities);
var container = _container.GetContainer(args.Mover, MoverContainer);
foreach (var servant in entities)
{
if (servant.Comp.King != ent)
continue;
_container.Insert(servant.Owner, container);
}
}
}

View File

@ -0,0 +1 @@
node-crawl-enter = Enter {$target}

View File

@ -928,7 +928,9 @@
- type: entity
name: crab
parent: SimpleMobBase
parent:
- SimpleMobBase
- DVNodeCrawler # DeltaV - node crawling
id: MobCrab
description: A folk legend goes around that his claw snaps spacemen out of existence over distasteful remarks. Be polite and tolerant for your own safety.
components:
@ -1835,7 +1837,9 @@
- type: entity
name: mouse
parent: SimpleMobBase
parent:
- SimpleMobBase
- DVNodeCrawler # DeltaV - node crawling
id: MobMouse
description: Squeak!
components:
@ -2228,7 +2232,9 @@
- type: entity
name: slug
parent: SimpleMobBase
parent:
- SimpleMobBase
- DVNodeCrawler # DeltaV - node crawling
id: MobSlug
description: And they called this a lizard?
components:
@ -2280,7 +2286,9 @@
- type: entity
name: frog
parent: SimpleMobBase
parent:
- SimpleMobBase
- DVNodeCrawler # DeltaV - node crawling
id: MobFrog
description: Hop hop hop. Lookin' moist.
components:
@ -2582,7 +2590,9 @@
# I have included a snake_hiss.ogg sound file so if you want to use that be my guest
- type: entity
name: snake
parent: SimpleMobBase
parent:
- SimpleMobBase
- DVNodeCrawler # DeltaV - node crawling
id: MobSnake
description: Hissss! Bites aren't poisonous.
components:
@ -2727,7 +2737,9 @@
Burn: -0.07
- type: entity
parent: MobSpiderBase
parent:
- MobSpiderBase
- DVNodeCrawler # DeltaV - node crawling
id: MobSpiderAngryBase
abstract: true
components:
@ -3593,7 +3605,7 @@
- type: entity
name: hamster
parent: [ SimpleMobBase, MobCombat, StripableInventoryBase]
parent: [ SimpleMobBase, MobCombat, StripableInventoryBase, DVNodeCrawler ] # DeltaV - node crawling
id: MobHamster
description: A cute, fluffy, robust hamster.
components:
@ -3820,7 +3832,7 @@
- type: entity
name: diona nymph
parent: [SimpleMobBase, StripableInventoryBase]
parent: [SimpleMobBase, StripableInventoryBase, DVNodeCrawler] # DeltaV - node crawling
id: MobDionaNymph
description: It's like a cat, only.... branch-ier.
components:

View File

@ -1,7 +1,7 @@
- type: entity
name: rat king
id: MobRatKing
parent: [ SimpleMobBase, MobCombat ]
parent: [ SimpleMobBase, MobCombat, DVNodeCrawler ] # DeltaV - node crawling
description: He's da rat. He make da roolz.
components:
- type: CombatMode
@ -170,7 +170,7 @@
- type: entity
name: rat servant
id: MobRatServant
parent: [ SimpleMobBase, MobCombat ]
parent: [ SimpleMobBase, MobCombat, DVNodeCrawler ] # DeltaV - node crawling
description: He's da mini rat. He don't make da roolz.
categories: [ HideSpawnMenu ] #Must be configured to a King or the AI breaks.
components:

View File

@ -1,7 +1,7 @@
- type: entity
name: basic slime
id: BaseMobAdultSlimes
parent: [ SimpleMobBase, MobCombat ]
parent: [ SimpleMobBase, MobCombat, DVNodeCrawler ] # DeltaV - node crawling
abstract: true
description: It looks so much like jelly. I wonder what it tastes like?
components:

View File

@ -439,6 +439,7 @@
- type: GuideHelp
guides:
- GasCanisters
- type: CrawlableNode # DeltaV
- type: entity
parent: GasVentPump
@ -505,6 +506,7 @@
- type: GuideHelp
guides:
- AirVent
- type: NodeCrawlVentAccess # DeltaV
- type: entity
parent: [ BaseMachine, ConstructibleMachine ]

View File

@ -80,6 +80,7 @@
Primary: Structures/Piping/Atmospherics/pipe.rsi
Secondary: Structures/Piping/Atmospherics/pipe_alt1.rsi
Tertiary: Structures/Piping/Atmospherics/pipe_alt2.rsi
- type: CrawlableNode # DeltaV
#Note: The PipeDirection of the PipeNode should be the south-facing version, because the entity starts at an angle of 0 (south)
@ -413,3 +414,4 @@
- type: Tag
tags:
- Unstackable
- type: CrawlableNode # DeltaV

View File

@ -83,6 +83,7 @@
Primary: GasVentPump
Secondary: GasVentPumpAlt1
Tertiary: GasVentPumpAlt2
- type: NodeCrawlVentAccess # DeltaV
- type: entity
parent: GasUnaryBase
@ -120,6 +121,7 @@
Primary: GasPassiveVent
Secondary: GasPassiveVentAlt1
Tertiary: GasPassiveVentAlt2
- type: NodeCrawlVentAccess # DeltaV
- type: entity
parent: [GasUnaryBase, AirSensorBase]
@ -184,6 +186,7 @@
Primary: GasVentScrubber
Secondary: GasVentScrubberAlt1
Tertiary: GasVentScrubberAlt2
- type: NodeCrawlVentAccess # DeltaV
- type: entity
parent: GasUnaryBase
@ -233,6 +236,7 @@
Primary: GasOutletInjector
Secondary: GasOutletInjectorAlt1
Tertiary: GasOutletInjectorAlt2
- type: NodeCrawlVentAccess # DeltaV
- type: entity
parent: [ BaseMachinePowered, ConstructibleMachine ]

View File

@ -0,0 +1,10 @@
- type: entity
abstract: true
id: DVNodeCrawler
components:
- type: NodeCrawler
revealedComponents:
- Content.Shared.Atmos.Components.PipeAppearanceComponent
exitNodes:
components:
- NodeCrawlVentAccess

View File

@ -0,0 +1,25 @@
- type: entity
categories: [ HideSpawnMenu ]
id: DVNodeCrawlMover
components:
- type: InputMover
- type: NodeCrawlerMovement
- type: Physics
bodyType: KinematicController
canCollide: false
- type: ContainerContainer
containers:
mover-container: !type:Container
- type: LagCompensation
- type: MovementSpeedModifier
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 0.35
density: 15
layer:
- GhostImpassable
- type: Input
context: "human"