footprints prediction (#5111)

* works

* shuffle stuff around

* no more fucky wuckies, fucky wuckies are GONE

* maybe this will just fix my problems?

* usings

* thats not how flying works

* review
This commit is contained in:
Milon 2026-01-04 17:37:08 +01:00 committed by GitHub
parent 3a051b65a3
commit 0d0d5a17de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 866 additions and 261 deletions

View File

@ -1,127 +0,0 @@
using Content.Server.Atmos.Components;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
// using Content.Shared.Standing;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server._EE.FootPrint;
public sealed class FootPrintsSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
private EntityQuery<TransformComponent> _transformQuery;
private EntityQuery<MobThresholdsComponent> _mobThresholdQuery;
private EntityQuery<AppearanceComponent> _appearanceQuery;
// private EntityQuery<LayingDownComponent> _layingQuery;
public override void Initialize()
{
base.Initialize();
_transformQuery = GetEntityQuery<TransformComponent>();
_mobThresholdQuery = GetEntityQuery<MobThresholdsComponent>();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
// _layingQuery = GetEntityQuery<LayingDownComponent>();
SubscribeLocalEvent<FootPrintsComponent, ComponentStartup>(OnStartupComponent);
SubscribeLocalEvent<FootPrintsComponent, MoveEvent>(OnMove);
}
private void OnStartupComponent(EntityUid uid, FootPrintsComponent component, ComponentStartup args)
{
component.StepSize = Math.Max(0f, component.StepSize + _random.NextFloat(-0.05f, 0.05f));
}
private void OnMove(EntityUid uid, FootPrintsComponent component, ref MoveEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (component.PrintsColor.A <= 0f
|| !_transformQuery.TryComp(uid, out var transform)
|| !_mobThresholdQuery.TryComp(uid, out var mobThreshHolds)
|| !_map.TryFindGridAt(_transform.GetMapCoordinates((uid, transform)), out var gridUid, out _))
return;
var dragging = mobThreshHolds.CurrentThresholdState is MobState.Critical or MobState.Dead;
var distance = (transform.LocalPosition - component.StepPos).Length();
var stepSize = dragging ? component.DragSize : component.StepSize;
if (!(distance > stepSize))
return;
component.RightStep = !component.RightStep;
var entity = Spawn(component.StepProtoId, CalcCoords(gridUid, component, transform, dragging));
var footPrintComponent = EnsureComp<FootPrintComponent>(entity);
footPrintComponent.PrintOwner = uid;
Dirty(entity, footPrintComponent);
if (_appearanceQuery.TryComp(entity, out var appearance))
{
_appearance.SetData(entity, FootPrintVisualState.State, PickState(uid, dragging), appearance);
_appearance.SetData(entity, FootPrintVisualState.Color, component.PrintsColor, appearance);
}
if (!_transformQuery.TryComp(entity, out var stepTransform))
return;
stepTransform.LocalRotation = dragging
? (transform.LocalPosition - component.StepPos).ToAngle() + Angle.FromDegrees(-90f)
: transform.LocalRotation + Angle.FromDegrees(180f);
component.PrintsColor = component.PrintsColor.WithAlpha(Math.Max(0f, component.PrintsColor.A - component.ColorReduceAlpha));
component.StepPos = transform.LocalPosition;
if (!TryComp<SolutionContainerManagerComponent>(entity, out var solutionContainer)
|| !_solution.ResolveSolution((entity, solutionContainer), footPrintComponent.SolutionName, ref footPrintComponent.Solution, out var solution)
|| string.IsNullOrWhiteSpace(component.ReagentToTransfer) || solution.Volume >= 1)
return;
_solution.TryAddReagent(footPrintComponent.Solution.Value, component.ReagentToTransfer, 0.01, out _); //was 1
}
private EntityCoordinates CalcCoords(EntityUid uid, FootPrintsComponent component, TransformComponent transform, bool state)
{
if (state)
return new EntityCoordinates(uid, transform.LocalPosition);
var offset = component.RightStep
? new Angle(Angle.FromDegrees(180f) + transform.LocalRotation).RotateVec(component.OffsetPrint)
: new Angle(transform.LocalRotation).RotateVec(component.OffsetPrint);
return new EntityCoordinates(uid, transform.LocalPosition + offset);
}
private FootPrintVisuals PickState(EntityUid uid, bool dragging)
{
var state = FootPrintVisuals.BareFootPrint;
if (_inventory.TryGetSlotEntity(uid, "shoes", out _))
state = FootPrintVisuals.ShoesPrint;
if (_inventory.TryGetSlotEntity(uid, "outerClothing", out var suit) && TryComp<PressureProtectionComponent>(suit, out _))
state = FootPrintVisuals.SuitPrint;
if (dragging)
state = FootPrintVisuals.Dragging;
return state;
}
}

View File

@ -1,57 +0,0 @@
using System.Linq;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Fluids;
using Content.Shared.Fluids.Components;
using Robust.Shared.Physics.Events;
namespace Content.Server._EE.FootPrint;
public sealed class PuddleFootPrintsSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PuddleFootPrintsComponent, EndCollideEvent>(OnStepTrigger);
}
private void OnStepTrigger(EntityUid uid, PuddleFootPrintsComponent component, ref EndCollideEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (!TryComp<AppearanceComponent>(uid, out var appearance)
|| !TryComp<PuddleComponent>(uid, out var puddle)
|| !TryComp<FootPrintsComponent>(args.OtherEntity, out var tripper)
|| !TryComp<SolutionContainerManagerComponent>(uid, out var solutionManager)
|| !_solutionContainer.ResolveSolution((uid, solutionManager), puddle.SolutionName, ref puddle.Solution, out var solutions))
return;
var totalSolutionQuantity = solutions.Contents.Sum(sol => (float) sol.Quantity);
var waterQuantity = (from sol in solutions.Contents where sol.Reagent.Prototype == "Water" select (float) sol.Quantity).FirstOrDefault();
if (waterQuantity / (totalSolutionQuantity / 100f) > component.OffPercent || solutions.Contents.Count <= 0)
return;
tripper.ReagentToTransfer =
solutions.Contents.Aggregate((l, r) => l.Quantity > r.Quantity ? l : r).Reagent.Prototype;
if (_appearance.TryGetData(uid, PuddleVisuals.SolutionColor, out var color, appearance)
&& _appearance.TryGetData(uid, PuddleVisuals.CurrentVolume, out var volume, appearance))
AddColor((Color) color, (float) volume * component.SizeRatio, tripper);
_solutionContainer.RemoveEachReagent(puddle.Solution.Value, 0.01); //was 1
}
private void AddColor(Color col, float quantity, FootPrintsComponent component)
{
component.PrintsColor = component.ColorQuantity == 0f ? col : Color.InterpolateBetween(component.PrintsColor, col, component.ColorInterpolationFactor);
component.ColorQuantity += quantity;
}
}

View File

@ -112,6 +112,27 @@ public sealed partial class DCCVars
public static readonly CVarDef<int> YearOffset =
CVarDef.Create("game.current_year_offset", 550, CVar.SERVERONLY);
/*
* Footprints
*/
/// <summary>
/// Maximum number of footprints allowed per tile.
/// Won't allow for new footprints to spawn on the tile once reached.
/// Set to 0 to disable per-tile limiting.
/// </summary>
public static readonly CVarDef<int> MaxFootPrintsPerTile =
CVarDef.Create("footprints.max_per_tile", 2, CVar.REPLICATED);
/// <summary>
/// Maximum total number of footprints allowed on a single grid.
/// When this limit is reached, the oldest footprint on the grid will be deleted.
/// Set to 0 to disable global limiting.
/// </summary>
public static readonly CVarDef<int> MaxFootPrintsPerGrid =
CVarDef.Create("footprints.max_per_grid", 1000, CVar.REPLICATED);
/*
* Feedback webhook
*/

View File

@ -1,23 +1,30 @@
using Content.Shared.Chemistry.Components;
using Content.Shared._EE.FootPrint.Systems;
using Content.Shared.Chemistry.Components;
using Robust.Shared.GameStates;
namespace Content.Shared._EE.FootPrint;
/// <summary>
/// This is used for marking footsteps, handling footprint drawing.
/// Component attached to individual footprint entities spawned on the ground.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(FootPrintsSystem))]
public sealed partial class FootPrintComponent : Component
{
/// <summary>
/// Owner (with <see cref="FootPrintsComponent"/>) of a print (this component).
/// The entity that created this footprint (must have <see cref="FootPrintsComponent"/>).
/// </summary>
[AutoNetworkedField]
[DataField, AutoNetworkedField]
public EntityUid PrintOwner;
/// <summary>
/// Name of the solution container for reagent residue left in the footprint.
/// </summary>
[DataField]
public string SolutionName = "step";
/// <summary>
/// The solution container for this footprint's reagent residue.
/// </summary>
[ViewVariables]
public Entity<SolutionComponent>? Solution;
}

View File

@ -2,24 +2,58 @@
namespace Content.Shared._EE.FootPrint;
/// <summary>
/// Visual states for different types of footprints.
/// </summary>
[Serializable, NetSerializable]
public enum FootPrintVisuals : byte
{
/// <summary>
/// Bare foot footprints (no shoes).
/// </summary>
BareFootPrint,
/// <summary>
/// Regular shoe footprints.
/// </summary>
ShoesPrint,
/// <summary>
/// Hardsuit footprints.
/// </summary>
SuitPrint,
/// <summary>
/// Drag marks from being pulled/dragged.
/// </summary>
Dragging
}
/// <summary>
/// Appearance data keys for footprint visuals.
/// </summary>
[Serializable, NetSerializable]
public enum FootPrintVisualState : byte
{
/// <summary>
/// The current visual state (type of print).
/// </summary>
State,
/// <summary>
/// The color of the footprint.
/// </summary>
Color
}
/// <summary>
/// Sprite layers for footprint rendering.
/// </summary>
[Serializable, NetSerializable]
public enum FootPrintVisualLayers : byte
{
/// <summary>
/// The footprint sprite layer.
/// </summary>
Print
}

View File

@ -1,29 +1,42 @@
using System.Numerics;
using Content.Shared._EE.FootPrint.Systems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared._EE.FootPrint;
[RegisterComponent]
/// <summary>
/// Component for entities that can leave footprints as they move.
/// Tracks color state, position, and configuration for footprint generation.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(FootPrintsSystem), typeof(PuddleFootPrintsSystem))]
[AutoGenerateComponentState(fieldDeltas: true)]
public sealed partial class FootPrintsComponent : Component
{
[ViewVariables(VVAccess.ReadOnly), DataField]
public ResPath RsiPath = new("/Textures/_EE/Effects/footprints.rsi"); // DeltaV moved to its own space
/// <summary>
/// Path to the RSI containing footprint sprites.
/// </summary>
[DataField]
public ResPath RsiPath = new("/Textures/_EE/Effects/footprints.rsi");
// all of those are set as a layer
[ViewVariables(VVAccess.ReadOnly), DataField]
#region Sprite State Names
[DataField]
public string LeftBarePrint = "footprint-left-bare-human";
[ViewVariables(VVAccess.ReadOnly), DataField]
[DataField]
public string RightBarePrint = "footprint-right-bare-human";
[ViewVariables(VVAccess.ReadOnly), DataField]
[DataField]
public string ShoesPrint = "footprint-shoes";
[ViewVariables(VVAccess.ReadOnly), DataField]
[DataField]
public string SuitPrint = "footprint-suit";
[ViewVariables(VVAccess.ReadOnly), DataField]
[DataField]
public string[] DraggingPrint =
[
"dragging-1",
@ -32,57 +45,83 @@ public sealed partial class FootPrintsComponent : Component
"dragging-4",
"dragging-5",
];
// yea, those
[ViewVariables(VVAccess.ReadOnly), DataField]
#endregion
/// <summary>
/// Prototype ID for spawned footprint entities.
/// </summary>
[DataField]
public EntProtoId<FootPrintComponent> StepProtoId = "Footstep";
[ViewVariables(VVAccess.ReadOnly), DataField]
/// <summary>
/// The amount of solution to transfer with each footprint when stepping into a puddle.
/// </summary>
[DataField]
public FixedPoint2 AmountToTransfer = 0.01;
/// <summary>
/// Current color of footprints being left. Alpha channel determines visibility.
/// </summary>
[DataField, AutoNetworkedField]
public Color PrintsColor = Color.FromHex("#00000000");
/// <summary>
/// The size scaling factor for footprint steps. Must be positive.
/// The size scaling factor for footprint steps. Must be positive.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float StepSize = 0.7f;
/// <summary>
/// The size scaling factor for drag marks. Must be positive.
/// The size scaling factor for drag marks. Must be positive.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float DragSize = 0.5f;
/// <summary>
/// The amount of color to transfer from the source (e.g., puddle) to the footprint.
/// The total amount of color accumulated from stepping in puddles.
/// Used to determine when color should start fading.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float ColorQuantity;
/// <summary>
/// The factor by which the alpha channel is reduced in subsequent footprints.
/// The factor by which the alpha channel is reduced with each footprint.
/// Higher values = faster fading.
/// </summary>
[DataField]
public float ColorReduceAlpha = 0.1f;
[DataField]
public string? ReagentToTransfer;
/// <summary>
/// The reagent ID to transfer to footprint entities, set when stepping in puddles.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<ReagentPrototype>? ReagentToTransfer;
/// <summary>
/// Offset applied to footprints perpendicular to movement direction.
/// Creates the left/right alternating pattern.
/// </summary>
[DataField]
public Vector2 OffsetPrint = new(0.1f, 0f);
/// <summary>
/// Tracks which foot should make the next print. True for right foot, false for left.
/// Tracks which foot should make the next print. True for right foot, false for left.
/// </summary>
[DataField, AutoNetworkedField]
public bool RightStep = true;
/// <summary>
/// The position of the last footprint in world coordinates.
/// The position of the last footprint in local coordinates.
/// Used to determine when enough distance has been traveled for the next print.
/// </summary>
[DataField, AutoNetworkedField]
public Vector2 StepPos = Vector2.Zero;
/// <summary>
/// Controls how quickly the footprint color transitions between steps.
/// Value between 0 and 1, where higher values mean faster color changes.
/// Controls how quickly the footprint color transitions when stepping in new puddles.
/// Value between 0 and 1, where higher values mean faster color changes.
/// </summary>
[DataField]
public float ColorInterpolationFactor = 0.2f;
}

View File

@ -0,0 +1,80 @@
using Content.Shared._EE.FootPrint.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._EE.FootPrint;
/// <summary>
/// Component attached to grids to track footprints per tile.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(FootPrintsSystem))]
public sealed partial class GridFootPrintsComponent : Component
{
/// <summary>
/// Tracks all footprints on this grid, organized by tile position.
/// NOTE: Not auto-networked - we use custom delta states instead
/// </summary>
[DataField]
public Dictionary<Vector2i, List<NetEntity>> FootPrintsByTile = new();
/// <summary>
/// Total count of all footprints on this grid.
/// </summary>
[DataField]
public int TotalFootPrints;
/// <summary>
/// Tracks which tiles have been modified since the last state send.
/// </summary>
[ViewVariables]
public HashSet<Vector2i> DirtyTiles = new();
/// <summary>
/// Tracks which tiles have been completely removed since the last state send.
/// </summary>
[ViewVariables]
public HashSet<Vector2i> RemovedTiles = new();
/// <summary>
/// If true, the next state will be a full state instead of delta.
/// </summary>
[ViewVariables]
public bool NeedFullState = true;
}
/// <summary>
/// Custom component state that supports delta compression.
/// Only sends changed tiles instead of the entire dictionary.
/// </summary>
[Serializable, NetSerializable]
public sealed class GridFootPrintsComponentState(
bool fullState,
Dictionary<Vector2i, List<NetEntity>>? modified,
HashSet<Vector2i>? removed,
int totalFootPrints)
: IComponentState
{
/// <summary>
/// If true, this is a full state sync. Client should replace entire dictionary.
/// If false, this is a delta - client should apply changes.
/// </summary>
public bool FullState = fullState;
/// <summary>
/// Modified or added tiles and their footprint lists.
/// For full state: contains all tiles.
/// For delta: contains only changed tiles.
/// </summary>
public Dictionary<Vector2i, List<NetEntity>>? Modified = modified;
/// <summary>
/// Tiles that have been completely cleared (no footprints remaining).
/// Only relevant for delta states.
/// </summary>
public HashSet<Vector2i>? Removed = removed;
/// <summary>
/// Total count of footprints on the grid.
/// </summary>
public int TotalFootPrints = totalFootPrints;
}

View File

@ -1,11 +1,24 @@
using Content.Shared._EE.FootPrint.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared._EE.FootPrint;
[RegisterComponent]
/// <summary>
/// Component for puddles that can leave footprints when stepped on.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(PuddleFootPrintsSystem))]
public sealed partial class PuddleFootPrintsComponent : Component
{
[ViewVariables()]
/// <summary>
/// The ratio of the puddle's volume that determines color intensity transferred to footprints.
/// </summary>
[DataField, AutoNetworkedField]
public float SizeRatio = 0.2f;
[ViewVariables()]
/// <summary>
/// Minimum percentage of water content required before the puddle will transfer footprints.
/// Prevents pure water puddles from leaving colored footprints.
/// </summary>
[DataField, AutoNetworkedField]
public float OffPercent = 80f;
}

View File

@ -0,0 +1,140 @@
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
namespace Content.Shared._EE.FootPrint.Systems;
public sealed partial class FootPrintsSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
private void InitializeStateHandling()
{
SubscribeLocalEvent<GridFootPrintsComponent, ComponentGetState>(OnGridFootPrintsGetState);
SubscribeLocalEvent<GridFootPrintsComponent, ComponentHandleState>(OnGridFootPrintsHandleState);
}
private void OnGridFootPrintsGetState(Entity<GridFootPrintsComponent> ent, ref ComponentGetState args)
{
// Determine if we should send full state or delta
// Send full state if:
// - Component explicitly needs it
// - This is the first send to this player (FromTick is very old)
// - Periodically for desync recovery
var sendFullState = ent.Comp.NeedFullState ||
args.FromTick == GameTick.Zero ||
args.FromTick < _timing.CurTick -
(uint)(TimeSpan.FromSeconds(30).Seconds * _timing.TickRate);
if (sendFullState)
{
// Send complete state
var fullData = new Dictionary<Vector2i, List<NetEntity>>(ent.Comp.FootPrintsByTile.Count);
foreach (var (tile, footprints) in ent.Comp.FootPrintsByTile)
{
// Deep copy the list to avoid reference sharing
fullData[tile] = new List<NetEntity>(footprints);
}
args.State = new GridFootPrintsComponentState(
fullState: true,
modified: fullData,
removed: null,
totalFootPrints: ent.Comp.TotalFootPrints
);
// Clear dirty tracking after full state send
ent.Comp.DirtyTiles.Clear();
ent.Comp.RemovedTiles.Clear();
ent.Comp.NeedFullState = false;
}
else
{
// Send delta state - only changed tiles
Dictionary<Vector2i, List<NetEntity>>? modifiedTiles = null;
HashSet<Vector2i>? removedTiles = null;
if (ent.Comp.DirtyTiles.Count > 0)
{
modifiedTiles = new Dictionary<Vector2i, List<NetEntity>>(ent.Comp.DirtyTiles.Count);
foreach (var tile in ent.Comp.DirtyTiles)
{
if (ent.Comp.FootPrintsByTile.TryGetValue(tile, out var footprints))
{
// Deep copy the list
modifiedTiles[tile] = new List<NetEntity>(footprints);
}
}
}
if (ent.Comp.RemovedTiles.Count > 0)
{
removedTiles = new HashSet<Vector2i>(ent.Comp.RemovedTiles);
}
// Only create state if there are actual changes
if (modifiedTiles != null || removedTiles != null)
{
args.State = new GridFootPrintsComponentState(
fullState: false,
modified: modifiedTiles,
removed: removedTiles,
totalFootPrints: ent.Comp.TotalFootPrints
);
// Clear dirty tracking after delta send
ent.Comp.DirtyTiles.Clear();
ent.Comp.RemovedTiles.Clear();
}
// If no changes, args.State stays null and nothing is sent
}
}
private void OnGridFootPrintsHandleState(Entity<GridFootPrintsComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not GridFootPrintsComponentState state)
return;
if (state.FullState)
{
// Full state sync - replace entire dictionary
ent.Comp.FootPrintsByTile.Clear();
if (state.Modified != null)
{
foreach (var (tile, footprints) in state.Modified)
{
ent.Comp.FootPrintsByTile[tile] = new List<NetEntity>(footprints);
}
}
}
else
{
// Delta state - apply changes
// Apply removed tiles
if (state.Removed != null)
{
foreach (var tile in state.Removed)
{
ent.Comp.FootPrintsByTile.Remove(tile);
}
}
// Apply modified/added tiles
if (state.Modified != null)
{
foreach (var (tile, footprints) in state.Modified)
{
ent.Comp.FootPrintsByTile[tile] = new List<NetEntity>(footprints);
}
}
}
// Always update total count
ent.Comp.TotalFootPrints = state.TotalFootPrints;
// Clear any local dirty tracking on client
ent.Comp.DirtyTiles.Clear();
ent.Comp.RemovedTiles.Clear();
}
}

View File

@ -0,0 +1,330 @@
using Content.Shared._EE.Flight;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Standing;
using Content.Shared.Tag;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Configuration;
using Content.Shared._DV.CCVars;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Shared._EE.FootPrint.Systems;
/// <summary>
/// Handles creation of footprints as entities move.
/// </summary>
public sealed partial class FootPrintsSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly TagSystem _tag = default!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
private EntityQuery<FlightComponent> _flightQuery;
private EntityQuery<GridFootPrintsComponent> _gridFootPrintsQuery;
private EntityQuery<MobThresholdsComponent> _mobThresholdQuery;
private EntityQuery<StandingStateComponent> _standingQuery;
private EntityQuery<TransformComponent> _transformQuery;
private const string HardsuitTag = "Hardsuit";
private int _maxPerTile;
private int _maxPerGrid;
public override void Initialize()
{
base.Initialize();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_flightQuery = GetEntityQuery<FlightComponent>();
_gridFootPrintsQuery = GetEntityQuery<GridFootPrintsComponent>();
_mobThresholdQuery = GetEntityQuery<MobThresholdsComponent>();
_standingQuery = GetEntityQuery<StandingStateComponent>();
_transformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<FootPrintsComponent, ComponentStartup>(OnStartupComponent);
SubscribeLocalEvent<FootPrintsComponent, MoveEvent>(OnMove);
SubscribeLocalEvent<FootPrintComponent, ComponentRemove>(OnFootPrintRemoved);
InitializeStateHandling();
// Subscribe to CVar changes
Subs.CVar(_cfg, DCCVars.MaxFootPrintsPerTile, value => _maxPerTile = value, true);
Subs.CVar(_cfg, DCCVars.MaxFootPrintsPerGrid, value => _maxPerGrid = value, true);
}
private void OnStartupComponent(Entity<FootPrintsComponent> ent, ref ComponentStartup args)
{
// Add slight random variation to step size for more natural-looking prints
ent.Comp.StepSize = Math.Max(0f, ent.Comp.StepSize + _random.NextFloat(-0.05f, 0.05f));
DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.StepSize));
}
private void OnMove(Entity<FootPrintsComponent> ent, ref MoveEvent args)
{
// Don't create footprints if flying
if (_flightQuery.TryComp(ent, out var flight) && _flight.IsFlying((ent, flight)))
return;
// Don't create footprints if color is fully transparent
if (ent.Comp.PrintsColor.A <= 0f)
return;
if (!_transformQuery.TryComp(ent, out var transform))
return;
if (!_mobThresholdQuery.TryComp(ent, out var mobThresholds))
return;
// Check if entity is being dragged (critical or dead)
var isCrit = mobThresholds.CurrentThresholdState is MobState.Critical or MobState.Dead;
var dragging = isCrit || _standingQuery.TryComp(ent, out var standingComp) && _standing.IsDown((ent, standingComp));
// Calculate distance traveled since last footprint
var distance = (transform.LocalPosition - ent.Comp.StepPos).Length();
var stepSize = dragging ? ent.Comp.DragSize : ent.Comp.StepSize;
// Not enough distance traveled yet - MOST COMMON EARLY RETURN
if (distance <= stepSize)
return;
// Need to be on a grid to leave footprints
if (!_map.TryFindGridAt(_transform.GetMapCoordinates((ent, transform)), out var gridUid, out var grid))
return;
// Calculate spawn coordinates
var coords = CalcCoords(gridUid, ent.Comp, transform, dragging);
// Get the tile position for limit checking
if (!_mapSystem.TryGetTileRef(gridUid, grid, coords, out var tileRef))
return;
var tilePos = tileRef.GridIndices;
// Check per-tile limit BEFORE spawning
// Use TryComp instead of EnsureComp to avoid adding component in hot path
if (_maxPerTile > 0 && _gridFootPrintsQuery.TryComp(gridUid, out var gridFootPrints))
{
if (gridFootPrints.FootPrintsByTile.TryGetValue(tilePos, out var existingFootPrints)
&& existingFootPrints.Count >= _maxPerTile)
{
// Tile is full, don't spawn
return;
}
}
// Alternate feet
ent.Comp.RightStep = !ent.Comp.RightStep;
DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.RightStep));
// Spawn the footprint entity
var entity = EntityManager.PredictedSpawnAtPosition(ent.Comp.StepProtoId.Id, coords);
// Update appearance with current color and state
if (_appearanceQuery.TryComp(entity, out var appearance))
{
_appearance.SetData(entity, FootPrintVisualState.State, PickState(ent, dragging), appearance);
_appearance.SetData(entity, FootPrintVisualState.Color, ent.Comp.PrintsColor, appearance);
}
// Set rotation
if (_transformQuery.TryComp(entity, out var stepTransform))
{
stepTransform.LocalRotation = dragging
? (transform.LocalPosition - ent.Comp.StepPos).ToAngle() + Angle.FromDegrees(-90f)
: transform.LocalRotation + Angle.FromDegrees(180f);
}
if (!TryComp<FootPrintComponent>(entity, out var footPrintComponent))
return;
// Set the owner reference
footPrintComponent.PrintOwner = ent;
Dirty(entity, footPrintComponent);
// Track the footprint on the grid
TrackFootPrint(gridUid, GetNetEntity(ent), tilePos);
// Reduce color alpha for next footprint
ent.Comp.PrintsColor = ent.Comp.PrintsColor.WithAlpha(
Math.Max(0f, ent.Comp.PrintsColor.A - ent.Comp.ColorReduceAlpha));
DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.PrintsColor));
// Update last step position
ent.Comp.StepPos = transform.LocalPosition;
DirtyField(ent.AsNullable(), nameof(FootPrintsComponent.StepPos));
// Handle reagent transfer
if (ent.Comp.ReagentToTransfer is { } reagent)
{
TryTransferReagent((entity, footPrintComponent), ent, reagent);
}
}
private void OnFootPrintRemoved(Entity<FootPrintComponent> ent, ref ComponentRemove args)
{
// Clean up tracking when footprint is deleted
if (!_transformQuery.TryComp(ent, out var transform))
return;
if (transform.GridUid == null)
return;
UntrackFootPrint(transform.GridUid.Value, GetNetEntity(ent));
}
private void TrackFootPrint(EntityUid gridUid, NetEntity footPrintUid, Vector2i tile)
{
var gridFootPrints = EnsureComp<GridFootPrintsComponent>(gridUid);
// Add to tile tracking
if (!gridFootPrints.FootPrintsByTile.TryGetValue(tile, out var tileFootPrints))
{
tileFootPrints = new List<NetEntity>();
gridFootPrints.FootPrintsByTile[tile] = tileFootPrints;
}
tileFootPrints.Add(footPrintUid);
gridFootPrints.TotalFootPrints++;
// DELTA TRACKING: Mark tile as dirty
gridFootPrints.DirtyTiles.Add(tile);
// Remove from removed set if it was there
gridFootPrints.RemovedTiles.Remove(tile);
// Enforce global limit
if (_maxPerGrid > 0 && gridFootPrints.TotalFootPrints > _maxPerGrid)
{
RemoveOldestFootPrint(gridFootPrints);
}
Dirty(gridUid, gridFootPrints);
}
private void UntrackFootPrint(EntityUid gridUid, NetEntity footPrintUid)
{
if (!_gridFootPrintsQuery.TryComp(gridUid, out var gridFootPrints))
return;
// Find and remove from tile tracking
foreach (var (tile, footPrints) in gridFootPrints.FootPrintsByTile)
{
if (footPrints.Remove(footPrintUid))
{
gridFootPrints.TotalFootPrints--;
// DELTA TRACKING
if (footPrints.Count == 0)
{
// Tile is now empty
gridFootPrints.FootPrintsByTile.Remove(tile);
gridFootPrints.RemovedTiles.Add(tile);
gridFootPrints.DirtyTiles.Remove(tile);
}
else
{
// Tile still has footprints, just modified
gridFootPrints.DirtyTiles.Add(tile);
}
Dirty(gridUid, gridFootPrints);
break;
}
}
}
private void RemoveOldestFootPrint(GridFootPrintsComponent gridFootPrints)
{
foreach (var (tile, footPrints) in gridFootPrints.FootPrintsByTile)
{
if (footPrints.Count > 0)
{
var toRemove = footPrints[0];
footPrints.RemoveAt(0);
QueueDel(GetEntity(toRemove));
gridFootPrints.TotalFootPrints--;
// DELTA TRACKING
if (footPrints.Count == 0)
{
gridFootPrints.FootPrintsByTile.Remove(tile);
gridFootPrints.RemovedTiles.Add(tile);
gridFootPrints.DirtyTiles.Remove(tile);
}
else
{
gridFootPrints.DirtyTiles.Add(tile);
}
return;
}
}
}
private void TryTransferReagent(Entity<FootPrintComponent> ent, Entity<FootPrintsComponent> tripper, ProtoId<ReagentPrototype> reagentId)
{
if (!TryComp<SolutionContainerManagerComponent>(ent, out var solutionContainer))
return;
if (!_solution.ResolveSolution((ent, solutionContainer),
ent.Comp.SolutionName,
ref ent.Comp.Solution,
out var solution))
return;
// Don't overfill
if (solution.Volume >= 1)
return;
// Transfer a small amount of reagent
_solution.TryAddReagent(ent.Comp.Solution.Value, reagentId, tripper.Comp.AmountToTransfer, out _);
}
private EntityCoordinates CalcCoords(EntityUid gridUid,
FootPrintsComponent component,
TransformComponent transform,
bool dragging)
{
// For dragging, place footprint at center
if (dragging)
return new EntityCoordinates(gridUid, transform.LocalPosition);
// For walking, offset left or right based on which foot
var offset = component.RightStep
? new Angle(Angle.FromDegrees(180f) + transform.LocalRotation).RotateVec(component.OffsetPrint)
: new Angle(transform.LocalRotation).RotateVec(component.OffsetPrint);
return new EntityCoordinates(gridUid, transform.LocalPosition + offset);
}
private FootPrintVisuals PickState(Entity<FootPrintsComponent> ent, bool dragging)
{
// If dragging, always use drag marks
if (dragging)
return FootPrintVisuals.Dragging;
// Check for shoes
if (_inventory.TryGetSlotEntity(ent, "shoes", out _))
return FootPrintVisuals.ShoesPrint;
// Check for hardsuit
if (_inventory.TryGetSlotEntity(ent, "outerClothing", out var suit) && _tag.HasTag(suit.Value, HardsuitTag))
return FootPrintVisuals.SuitPrint;
// Default to bare feet
return FootPrintVisuals.BareFootPrint;
}
}

View File

@ -0,0 +1,123 @@
using System.Linq;
using Content.Shared._EE.Flight;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Fluids;
using Content.Shared.Fluids.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
namespace Content.Shared._EE.FootPrint.Systems;
/// <summary>
/// Handles transferring puddle colors and reagents to entities with footprints when they step in puddles.
/// </summary>
public sealed class PuddleFootPrintsSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
private EntityQuery<FlightComponent> _flightQuery;
private static readonly ProtoId<ReagentPrototype> WaterPrototype = "Water";
public override void Initialize()
{
base.Initialize();
_flightQuery = GetEntityQuery<FlightComponent>();
SubscribeLocalEvent<PuddleFootPrintsComponent, EndCollideEvent>(OnEndCollide);
}
private void OnEndCollide(Entity<PuddleFootPrintsComponent> ent, ref EndCollideEvent args)
{
var tripper = args.OtherEntity;
// Don't process if the tripper is flying
if (_flightQuery.TryComp(ent, out var flight) && _flight.IsFlying((ent, flight)))
return;
// Only process entities that can leave footprints
if (!TryComp<FootPrintsComponent>(tripper, out var footPrints))
return;
// Get puddle appearance and solution data
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
if (!TryComp<PuddleComponent>(ent, out var puddleComp))
return;
if (!TryComp<SolutionContainerManagerComponent>(ent, out var solutionManager))
return;
if (!_solutionContainer.ResolveSolution((ent, solutionManager),
puddleComp.SolutionName,
ref puddleComp.Solution,
out var solution))
return;
// Calculate total solution quantity and water percentage
var totalSolutionQuantity = solution.Contents.Sum(sol => (float)sol.Quantity);
if (totalSolutionQuantity <= 0 || solution.Contents.Count <= 0)
return;
var waterQuantity = solution.Contents
.Where(sol => sol.Reagent.Prototype == WaterPrototype)
.Sum(sol => (float)sol.Quantity);
var waterPercent = (waterQuantity / totalSolutionQuantity) * 100f;
// If puddle is mostly water, don't transfer color
if (waterPercent > ent.Comp.OffPercent)
return;
// Find the reagent with the highest quantity to transfer
var primaryReagent = solution.Contents
.OrderByDescending(sol => sol.Quantity)
.FirstOrDefault();
if (primaryReagent.Reagent.Prototype == null)
return;
// Set the reagent to transfer
footPrints.ReagentToTransfer = primaryReagent.Reagent.Prototype;
// Transfer color from puddle to footprints
if (_appearance.TryGetData(ent, PuddleVisuals.SolutionColor, out var colorValue, appearance)
&& _appearance.TryGetData(ent, PuddleVisuals.CurrentVolume, out var volumeValue, appearance))
{
if (colorValue is Color color && volumeValue is float volume)
{
AddColor(color, volume * ent.Comp.SizeRatio, footPrints);
Dirty(tripper, footPrints);
}
}
// Remove small amount of reagent from puddle
_solutionContainer.RemoveEachReagent(puddleComp.Solution.Value, footPrints.AmountToTransfer);
}
private void AddColor(Color color, float quantity, FootPrintsComponent component)
{
// If no color yet, use the puddle's color directly
if (component.ColorQuantity == 0f)
{
component.PrintsColor = color;
}
else
{
// Interpolate between current color and new color
component.PrintsColor = Color.InterpolateBetween(
component.PrintsColor,
color,
component.ColorInterpolationFactor);
}
component.ColorQuantity += quantity;
}
}

View File

@ -215,47 +215,4 @@
- type: Tag
tags:
- DNASolutionScannable
- type: PuddleFootPrints # DeltaV- Begin updates from EE Blood Puddle
- type: entity
name: footstep
id: Footstep
save: false
description: Trace of liquid
components:
- type: Clickable
- type: FootstepModifier
footstepSoundCollection:
collection: FootstepWater
params:
volume: 3
- type: Transform
noRot: false
- type: Sprite
drawdepth: FloorObjects
color: "#FFFFFF80"
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
slipFixture:
shape:
!type:PhysShapeAabb
bounds: "-0.4,-0.4,0.4,0.4"
mask:
- ItemMask
layer:
- SlipLayer
hard: false
- type: SolutionContainerManager
solutions:
step: { maxVol: 2 }
- type: FootPrint
- type: Puddle
solution: step
- type: Appearance
- type: SpawnOnDespawn
prototype: PuddleSparkle
- type: TimedDespawn
lifetime: 60 # DeltaV End updates from EE Blood Puddle
- type: PuddleFootPrints # DeltaV

View File

@ -195,3 +195,48 @@
- id: PuddleRandomChems
- id: PuddleRandomKitchen
- id: PuddleRandomMisc
- type: entity
name: footstep
id: Footstep
save: false
description: Trace of liquid
components:
- type: Clickable
- type: FootstepModifier
footstepSoundCollection:
collection: FootstepWater
params:
volume: 3
- type: Transform
noRot: false
- type: Sprite
drawdepth: FloorObjects
color: "#FFFFFF80"
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
slipFixture:
shape:
!type:PhysShapeAabb
bounds: "-0.4,-0.4,0.4,0.4"
mask:
- ItemMask
layer:
- SlipLayer
hard: false
- type: SolutionContainerManager
solutions:
step: { maxVol: 2 }
- type: FootPrint
- type: Puddle
solution: step
- type: Appearance
- type: SpawnOnDespawn
prototype: PuddleSparkle
- type: SpeedModifierContacts
ignoreWhitelist:
components:
- FootPrints