This commit is contained in:
Sir Warock 2026-08-14 01:46:09 +00:00 committed by GitHub
commit ce1ee16629
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 1455 additions and 276 deletions

View File

@ -0,0 +1,78 @@
using Content.Client.Clothing;
using Content.Client.Items.Systems;
using Content.Shared._Funkystation.Stains.Components;
using Content.Shared._Funkystation.Stains.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Clothing;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client._Funkystation.Stains;
public sealed class StainSystem : SharedStainSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = null!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = null!;
[Dependency] private readonly SpriteSystem _sprite = null!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StainableComponent, AppearanceChangeEvent>(OnAppearanceChanged);
SubscribeLocalEvent<StainableComponent, GetEquipmentVisualsEvent>(OnEquipmentVisuals, after: [typeof(ClientClothingSystem)]);
SubscribeLocalEvent<StainableComponent, GetInhandVisualsEvent>(OnInhandVisuals, after: [typeof(ItemSystem)]);
}
private void OnAppearanceChanged(Entity<StainableComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
var spriteEnt = new Entity<SpriteComponent?>(ent.Owner, args.Sprite);
var layers = new List<int>(ent.Comp.RevealedLayers);
layers.Sort((a, b) => b.CompareTo(a));
foreach (var layer in layers)
{
_sprite.RemoveLayer(spriteEnt, layer);
}
ent.Comp.RevealedLayers.Clear();
foreach (var (_, layerData) in BuildVisuals(ent, ent.Comp.IconVisuals, "icon"))
{
#pragma warning disable CS0618
ent.Comp.RevealedLayers.Add(args.Sprite.AddLayer(layerData));
#pragma warning restore CS0618
}
}
private void OnEquipmentVisuals(Entity<StainableComponent> ent, ref GetEquipmentVisualsEvent args)
{
if (ent.Comp.ClothingVisuals.TryGetValue(args.Slot, out var layers))
args.Layers.AddRange(BuildVisuals(ent, layers, args.Slot));
}
private void OnInhandVisuals(Entity<StainableComponent> ent, ref GetInhandVisualsEvent args)
{
if (ent.Comp.ItemVisuals.TryGetValue(args.Location.ToString(), out var layers))
args.Layers.AddRange(BuildVisuals(ent, layers, args.Location.ToString()));
}
private IEnumerable<(string, PrototypeLayerData)> BuildVisuals(Entity<StainableComponent> ent, List<PrototypeLayerData> templates, string prefix)
{
if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var sol) || sol.Volume <= FixedPoint2.Zero)
yield break;
var color = sol.GetColor(_prototypeManager);
for (var i = 0; i < templates.Count; i++)
{
var layer = templates[i];
layer.Color = color;
yield return ($"stain-{prefix}-{i}", layer);
}
}
}

View File

@ -0,0 +1,8 @@
using Content.Shared._Funkystation.WashingMachine;
namespace Content.Client._Funkystation.WashingMachine;
/// <summary>
/// This only exists for client-side prediction.
/// </summary>
public sealed class WashingMachineSystem : SharedWashingMachineSystem;

View File

@ -38,5 +38,15 @@ namespace Content.Server.Construction.Components
// TODO Force flush interaction queue before serializing to YAML.
// Otherwise you can end up with entities stuck in invalid states (e.g., waiting for DoAfters).
public readonly Queue<object> InteractionQueue = new();
// DeltaV Start - WashingMachine shouldn't keep people inside when it breaks to a previous node.
/// <summary>
/// These containers should not be handled by construction and should resume their normal behavior.
/// </summary>
/// <example>Washing machines shouldn't transfer their entityStorage container to the previous node when broken,
/// but instead release their content.</example>
[DataField]
public HashSet<string>? ForbiddenContainers;
// DeltaV End.
}
}

View File

@ -29,7 +29,9 @@ namespace Content.Server.Construction
/// the entity does not have a <see cref="ConstructionComponent"/>.</returns>
public bool AddContainer(EntityUid uid, string container, ConstructionComponent? construction = null)
{
if (!Resolve(uid, ref construction))
if (!Resolve(uid, ref construction)
// DeltaV - Prohibit construction to manage specific containers, so it doesn't put contents into broken states.
|| construction.ForbiddenContainers != null && construction.ForbiddenContainers.Contains(container))
return false;
return construction.Containers.Add(container);

View File

@ -14,6 +14,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Maps;
using Content.Shared.Popups;
using Content.Shared.Slippery;
using Content.Shared._Funkystation.Fluids; // Funky - Stainable Clothing.
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@ -255,8 +256,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
if (!_random.Prob(0.5f))
return;
if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution,
out var solution))
if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution)) // Funky
return;
Popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)),
@ -265,6 +265,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Take 15% of the puddle solution
var splitSol = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, solution.Volume * 0.15f);
Reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
// Funky Start - Stain clothing on slip.
if (splitSol.Volume > 0)
{
var stainEv = new SpilledOnEvent(entity.Owner, splitSol.Clone());
RaiseLocalEvent(args.Slipped, stainEv);
}
// Funky End - Stain clothing on slip.
}
/// <summary>
@ -437,6 +444,10 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
targets.Add(owner);
Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
if (splitSolution.Volume > 0) // Funky - Stainable Clothing.
RaiseLocalEvent(owner, new SpilledOnEvent(entity, splitSolution.Clone()));
Popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other",
("spillable", entity),
("target", Identity.Entity(owner, EntityManager))),

View File

@ -1,45 +0,0 @@
using Robust.Shared.Containers;
using Content.Shared.Destructible;
using Content.Shared.Nyanotrasen.Laundry;
using Content.Shared.Storage;
namespace Content.Server.Nyanotrasen.Laundry;
// I just wanted the sprite to change states when it broke.
public sealed class LaundrySystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedWashingMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SharedWashingMachineComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<SharedWashingMachineComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<SharedWashingMachineComponent, EntRemovedFromContainerMessage>(OnContainerModified);
}
private void OnMapInit(EntityUid uid, SharedWashingMachineComponent component, MapInitEvent args)
{
if (!_containerSystem.TryGetContainer(uid, "storagebase", out var container))
return;
_appearanceSystem.SetData(uid, StorageVisuals.HasContents, container.ContainedEntities.Count > 0);
}
private void OnBreak(EntityUid uid, SharedWashingMachineComponent component, BreakageEventArgs args)
{
_appearanceSystem.SetData(uid, WashingMachineVisualState.Broken, true);
}
private void OnContainerModified(EntityUid uid, SharedWashingMachineComponent component, ContainerModifiedMessage args)
{
if (args.Container.ID == "storagebase")
_appearanceSystem.SetData(uid, StorageVisuals.HasContents, args.Container.ContainedEntities.Count > 0);
}
}

View File

@ -0,0 +1,19 @@
using Content.Shared._Funkystation.Stains.Components;
using Content.Shared._Funkystation.Stains.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Tag;
namespace Content.Server._Funkystation.Stains;
public sealed class StainSystem : SharedStainSystem
{
[Dependency] private readonly TagSystem _tag = null!;
private static readonly string ScannableDnaTag = "DNASolutionScannable";
protected override void OnStained(Entity<StainableComponent> ent, Entity<SolutionComponent> solution)
{
base.OnStained(ent, solution);
_tag.AddTag(ent.Owner, ScannableDnaTag);
}
}

View File

@ -0,0 +1,165 @@
using Content.Shared._Funkystation.WashingMachine;
using Content.Shared._Funkystation.Stains.Components;
using Content.Shared._Funkystation.Stains.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Storage.Components;
using Content.Server.Forensics;
using Content.Shared.Clothing.Components;
using Robust.Shared.Audio;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Chemistry;
using Content.Shared.Damage.Systems;
namespace Content.Server._Funkystation.WashingMachine;
public sealed class WashingMachineSystem : SharedWashingMachineSystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solution = null!;
[Dependency] private readonly SharedStainSystem _stains = null!;
[Dependency] private readonly ForensicsSystem _forensics = null!;
[Dependency] private readonly DamageableSystem _damageable = null!;
[Dependency] private readonly IRobustRandom _random = null!;
[Dependency] private readonly ReactiveSystem _reactive = null!;
private static readonly SoundSpecifier HitSound = new SoundCollectionSpecifier("MetalThud");
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WashingMachineComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<WashingMachineComponent> machine, ref MapInitEvent args)
{
Appearance.SetData(machine.Owner, WashingMachineVisuals.State, machine.Comp.State);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<WashingMachineComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.State != WashingMachineState.Washing || comp.NextWashingStep > Timing.CurTime)
continue;
if (Timing.CurTime >= comp.WashFinishTime)
{
FinishWash((uid, comp));
continue;
}
comp.NextWashingStep = Timing.CurTime + comp.WashingStepCooldown;
ProcessWashingHazards((uid, comp));
}
}
private void ProcessWashingHazards(Entity<WashingMachineComponent> machine)
{
if (!TryComp<EntityStorageComponent>(machine, out var storage) || storage.Contents.ContainedEntities.Count == 0)
return;
var reagentSpray = new Solution();
reagentSpray.AddReagent(machine.Comp.SprayReagent, machine.Comp.ReagentSprayAmount);
// We store them in a hashset as gibbing will modify the collection and cause an error.
var entitiesToWash = storage.Contents.ContainedEntities.ToHashSet();
var doSpray = _random.Prob(machine.Comp.ReagentSprayChance);
var hasHeavyItems = false;
foreach (var item in entitiesToWash)
{
_damageable.TryChangeDamage(item, machine.Comp.EntityBluntDamage, true);
if (doSpray)
_reactive.DoEntityReaction(item, reagentSpray, ReactionMethod.Touch);
if (!hasHeavyItems && !HasComp<ClothingComponent>(item))
hasHeavyItems = true;
}
if (hasHeavyItems && _random.Prob(machine.Comp.ThumpSoundChance))
Audio.PlayPvs(HitSound, machine);
}
protected override bool TryStartWash(Entity<WashingMachineComponent> machine, EntityUid user)
{
if (!base.TryStartWash(machine, user))
return false;
machine.Comp.AudioStream = Audio.PlayPvs(machine.Comp.WashLoopSound, machine.Owner)?.Entity;
return true;
}
private void FinishWash(Entity<WashingMachineComponent> machine)
{
machine.Comp.State = WashingMachineState.Idle;
machine.Comp.WashFinishTime = null;
machine.Comp.NextWashAllowed = Timing.CurTime + machine.Comp.Cooldown;
Audio.Stop(machine.Comp.AudioStream);
Audio.PlayPvs(machine.Comp.WashFinishedSound, machine);
Appearance.SetData(machine, WashingMachineVisuals.State, WashingMachineState.Idle);
var hasHeavyItems = false;
HashSet<EntityUid> items = new();
if (TryComp<EntityStorageComponent>(machine, out var storage))
{
items = storage.Contents.ContainedEntities.ToHashSet();
foreach (var item in items)
{
if (!hasHeavyItems && !HasComp<ClothingComponent>(item))
hasHeavyItems = true;
if (!TryComp<StainableComponent>(item, out var stain)
|| !_solution.TryGetSolution(item, stain.SolutionName, out var sol))
continue;
if (TryComp<ForensicsComponent>(machine, out var machineForensics))
machineForensics.DNAs.UnionWith(_forensics.GetSolutionsDNA(sol.Value.Comp.Solution));
_solution.RemoveAllSolution(sol.Value);
_stains.UpdateVisuals((item, stain));
}
}
var machineEv = new WashingMachineFinishedWashingEvent(items);
RaiseLocalEvent(machine, machineEv);
var itemEv = new WashingMachineWashedEvent(machine, items);
foreach (var item in items)
{
RaiseLocalEvent(item, itemEv);
}
UpdateForensics((machine, machine), items);
if (hasHeavyItems && machine.Comp.SelfDamage.AnyPositive())
{
_damageable.TryChangeDamage(machine.Owner, machine.Comp.SelfDamage * machine.Comp.WashTime.TotalSeconds, ignoreResistances: true);
}
Storage.OpenStorage(machine);
Dirty(machine);
}
private void UpdateForensics(Entity<WashingMachineComponent> machine, HashSet<EntityUid> items)
{
if (!TryComp<ForensicsComponent>(machine.Owner, out var forensics))
return;
foreach (var item in items)
{
if (!TryComp<FiberComponent>(item, out var fiber))
continue;
var fiberLocale = string.IsNullOrEmpty(fiber.FiberColor)
? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial))
: Loc.GetString("forensic-fibers-colored", ("color", fiber.FiberColor), ("material", fiber.FiberMaterial));
forensics.Fibers.Add(fiberLocale + " ; " + fiber.Fiberprint);
}
}
}

View File

@ -13,11 +13,13 @@ using Content.Shared.Fluids;
using Content.Shared.Forensics.Components;
using Content.Shared.Gibbing;
using Content.Shared.HealthExaminable;
using Content.Shared.Inventory; // Funky - Stainable Clothing.
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Rejuvenate;
using Content.Shared.StatusEffectNew;
using Content.Shared._Funkystation.Fluids; // Funky - Stainable Clothing.
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
@ -40,6 +42,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; // Funky - Stainable Clothing.
public override void Initialize()
{
@ -461,7 +464,22 @@ public abstract class SharedBloodstreamSystem : EntitySystem
return true;
tempSolution.AddSolution(leakedBlood, PrototypeManager);
// Funky Start - Stainable Clothing.
var stainEv = new SpilledOnEvent(ent.Owner, tempSolution, ignoreBlockers: true);
RaiseLocalEvent(ent.Owner, stainEv);
var xform = Transform(ent.Owner);
foreach (var neighbor in _lookup.GetEntitiesInRange(xform.Coordinates, 1.5f))
{
if (neighbor == ent.Owner || !HasComp<InventoryComponent>(neighbor))
continue;
RaiseLocalEvent(neighbor, new SpilledOnEvent(ent.Owner, tempSolution));
if (tempSolution.Volume <= 0)
break;
}
// Funky End - Stainable Clothing.
if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold)
{
_puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false);
@ -522,7 +540,22 @@ public abstract class SharedBloodstreamSystem : EntitySystem
tempSol.AddSolution(tempSolution, PrototypeManager);
SolutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value);
}
// Funky Start - Stainable clothing.
var stainEv = new SpilledOnEvent(ent.Owner, tempSol, ignoreBlockers: true);
RaiseLocalEvent(ent.Owner, stainEv);
var xform = Transform(ent.Owner);
foreach (var neighbor in _lookup.GetEntitiesInRange(xform.Coordinates, 1.5f))
{
if (neighbor == ent.Owner || !HasComp<InventoryComponent>(neighbor))
continue;
RaiseLocalEvent(neighbor, new SpilledOnEvent(ent.Owner, tempSol));
if (tempSol.Volume <= 0)
break;
}
// Funky End - Stainable Clothing.
_puddle.TrySpillAt(ent, tempSol, out _);
}

View File

@ -17,6 +17,7 @@ using Content.Shared.Spillable;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared._Funkystation.Fluids; // Funky - Stainable Clothing.
using Robust.Shared.Player;
@ -166,7 +167,14 @@ public abstract partial class SharedPuddleSystem
continue;
var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount);
// Funky Start - Stainable Clothing.
if (splitSolution.Volume > 0)
{
// TODO: Remove the .Clone() when splashing someone doesn't evaporate the reagent anymore.
var stainEv = new SpilledOnEvent(entity.Owner, splitSolution.Clone());
RaiseLocalEvent(hit, stainEv);
}
// Funky End - Stainable Clothing.
AdminLogger.Add(LogType.MeleeHit,
$"{ToPrettyString(args.User):actor} "
+ $"splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} "

View File

@ -1,5 +1,6 @@
using Content.Shared._DV.Overlays; // DeltaV
using Content.Shared._DV.Psionics.Events; // DeltaV
using Content.Shared._Funkystation.Fluids; // Funky - Stainable Clothing
using Content.Shared.Armor;
using Content.Shared.Atmos;
using Content.Shared.Chat;
@ -118,6 +119,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<InnateVerb>>(OnGetInnateVerbs);
SubscribeLocalEvent<InventoryComponent, SpilledOnEvent>(RelayInventoryEvent); // Funky - Stainable Clothing.
}
protected void RefRelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent

View File

@ -12,6 +12,7 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared._Funkystation.Fluids; // Funky
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
@ -120,7 +121,10 @@ public sealed class VomitSystem : EntitySystem
// Makes a vomit solution the size of 90% of the chemicals removed from the chemstream
solution.AddReagent(new ReagentId(VomitPrototype, _bloodstream.GetEntityBloodData((uid, bloodStream))), vomitAmount);
}
// Funky Start - Stainable Clothing.
var stainEv = new SpilledOnEvent(uid, solution.Clone());
RaiseLocalEvent(uid, stainEv);
// Funky End - Stainable Clothing.
if (_puddle.TrySpillAt(uid, solution, out var puddle, false))
{
_forensics.TransferDna(puddle, uid, false);

View File

@ -1,12 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Laundry;
[RegisterComponent]
public sealed partial class SharedWashingMachineComponent : Component { } //Hi, I'm no coder but the word "partial" used to be "sealed" o3o
[Serializable, NetSerializable]
public enum WashingMachineVisualState : byte
{
Broken,
}

View File

@ -0,0 +1,36 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Inventory;
using Content.Shared.Trigger.Components.Effects;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Trigger.Components;
/// <summary>
/// Causes a spill on the entity when triggered.
/// If targetUser is true, it'll spill on the user instead.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class SpillOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// The name of the solution which will be used to spill on trigger.
/// </summary>
[DataField(required: true, tag: "solution")]
public string SolutionName;
/// <summary>
/// The inventory slots that this spill will be relayed to.
/// </summary>
[DataField]
public SlotFlags TargetSlots = SlotFlags.WITHOUT_POCKET;
/// <summary>
/// The inventory slots that this spill will be relayed to if the target is prone.
/// If left null, it'll copy the normal TargetSlots.
/// </summary>
[DataField("proneTargetSlots")]
private SlotFlags? _proneTargetSlots;
[ViewVariables]
public SlotFlags ProneTargetSlots => _proneTargetSlots ??= TargetSlots;
}

View File

@ -1,8 +1,8 @@
using Content.Shared._DV.Trigger.Components.Effects;
using Content.Shared.Trigger;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Trigger;
namespace Content.Shared._DV.Trigger.Systems;
namespace Content.Shared._DV.Trigger.Systems.OnTriggerSystems;
public sealed class CreateHitmanCardOnTriggerSystem : EntitySystem
{

View File

@ -3,7 +3,7 @@ using Content.Shared._DV.Trigger.Components.Effects;
using Content.Shared.Trigger;
using Robust.Shared.Player;
namespace Content.Shared._DV.Trigger.Systems;
namespace Content.Shared._DV.Trigger.Systems.OnTriggerSystems;
public sealed class ShowTipOnTriggerSystem : EntitySystem
{

View File

@ -0,0 +1,29 @@
using Content.Shared._DV.Trigger.Components;
using Content.Shared._Funkystation.Fluids;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Stunnable;
using Content.Shared.Trigger;
namespace Content.Shared._DV.Trigger.Systems.OnTriggerSystems;
public sealed class SpillOnTriggerSystem : XOnTriggerSystem<SpillOnTriggerComponent>
{
[Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
protected override void OnTrigger(Entity<SpillOnTriggerComponent> spiller, EntityUid target, ref TriggerEvent args)
{
if (!_solutionContainer.TryGetSolution(spiller.Owner, spiller.Comp.SolutionName, out var solution)
|| solution.Value.Comp.Solution.Volume <= FixedPoint2.Zero)
return;
var targetSlots = HasComp<KnockedDownComponent>(target)
? spiller.Comp.ProneTargetSlots
: spiller.Comp.TargetSlots;
var spilledEvent = new SpilledOnEvent(spiller, solution.Value.Comp.Solution, targetSlots);
RaiseLocalEvent(target, spilledEvent);
args.Handled = true;
}
}

View File

@ -0,0 +1,16 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Inventory;
namespace Content.Shared._Funkystation.Fluids;
/// <summary>
/// Raised when a fluid is spilled on an entity
/// </summary>
public sealed class SpilledOnEvent(EntityUid source, Solution solution, SlotFlags slotFlags = SlotFlags.WITHOUT_POCKET, bool ignoreBlockers = false) : EntityEventArgs, IInventoryRelayEvent
{
public EntityUid Source = source;
public Solution Solution = solution;
public bool IgnoreBlockers = ignoreBlockers;
public SlotFlags TargetSlots => slotFlags;
}

View File

@ -0,0 +1,17 @@
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
namespace Content.Shared._Funkystation.Stains.Components;
/// <summary>
/// Prevents entities equipped in specific slots underneath this item from getting stained
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StainBlockerComponent : Component
{
/// <summary>
/// These are the slots protected from stains by the entity with the component.
/// </summary>
[DataField("slots", required: true)]
public SlotFlags BlockedSlots;
}

View File

@ -0,0 +1,52 @@
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._Funkystation.Stains.Components;
/// <summary>
/// This lets clothing be stained by blood or other reagents that aren't water.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StainableComponent : Component
{
/// <summary>
/// The solution name. Pretty self-describing.
/// </summary>
[DataField]
public string SolutionName = "stain";
/// <summary>
/// How much units of reagents the solution can take.
/// </summary>
[DataField]
public FixedPoint2 MaxStainVolume = FixedPoint2.New(5);
/// <summary>
/// The amount of units that get added to the solution with every spill on it.
/// </summary>
[DataField]
public FixedPoint2 SpillTransferAmount = 0.5f;
/// <summary>
/// The doafter duration for removing the reagent from the solution by wringing it onto the floor.
/// </summary>
[DataField]
public float WringDoAfterDuration = 15f;
[DataField]
public Dictionary<string, List<PrototypeLayerData>> ClothingVisuals = new();
[DataField]
public Dictionary<string, List<PrototypeLayerData>> ItemVisuals = new();
[DataField]
public List<PrototypeLayerData> IconVisuals = new();
[ViewVariables]
public HashSet<int> RevealedLayers = new();
}
[Serializable, NetSerializable]
public sealed partial class WringStainDoAfterEvent : SimpleDoAfterEvent;

View File

@ -0,0 +1,177 @@
using Content.Shared._Funkystation.Fluids;
using Content.Shared._Funkystation.Stains.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared._Funkystation.Stains.Systems;
public abstract class SharedStainSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solution = null!;
[Dependency] private readonly SharedItemSystem _item = null!;
[Dependency] private readonly SharedAppearanceSystem _appearance = null!;
[Dependency] private readonly SharedContainerSystem _container = null!;
[Dependency] private readonly InventorySystem _inventory = null!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = null!;
[Dependency] private readonly SharedPuddleSystem _puddle = null!;
[Dependency] private readonly SharedPopupSystem _popup = null!;
private EntityQuery<StainBlockerComponent> _stainBlockerQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StainableComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StainableComponent, InventoryRelayedEvent<SpilledOnEvent>>(OnSpilledOn);
SubscribeLocalEvent<StainableComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<StainableComponent, WringStainDoAfterEvent>(OnWring);
SubscribeLocalEvent<StainableComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
_stainBlockerQuery = GetEntityQuery<StainBlockerComponent>();
}
private void OnSolutionChanged(Entity<StainableComponent> ent, ref SolutionContainerChangedEvent args)
{
if (args.SolutionId == ent.Comp.SolutionName)
UpdateVisuals(ent);
}
private void OnMapInit(Entity<StainableComponent> ent, ref MapInitEvent args)
{
if (_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out var sol))
_solution.SetCanReact(sol.Value, false);
}
private void OnSpilledOn(Entity<StainableComponent> clothing, ref InventoryRelayedEvent<SpilledOnEvent> args)
{
if (!args.Args.IgnoreBlockers && IsStainBlocked(clothing))
return;
if (!_solution.TryGetSolution(clothing.Owner, clothing.Comp.SolutionName, out var stainSolution))
return;
var attemptedTransferAmount = FixedPoint2.Min(args.Args.Solution.Volume, clothing.Comp.SpillTransferAmount);
var actualTransferAmount = FixedPoint2.Min(attemptedTransferAmount, stainSolution.Value.Comp.Solution.AvailableVolume);
// Exit early if nothing can be transferred.
if (actualTransferAmount == 0)
return;
var split = args.Args.Solution.SplitSolution(actualTransferAmount);
for (var i = split.Contents.Count - 1; i >= 0; i--)
{
if (split.Contents[i].Reagent.Prototype == "Water")
split.RemoveReagent(split.Contents[i].Reagent, split.Contents[i].Quantity);
}
if (split.Volume > 0)
{
_solution.TryAddSolution(stainSolution.Value, split);
UpdateVisuals(clothing);
OnStained(clothing, stainSolution.Value);
}
}
protected virtual void OnStained(Entity<StainableComponent> ent, Entity<SolutionComponent> solution) { }
private bool IsStainBlocked(Entity<StainableComponent> ent)
{
if (!_container.TryGetContainingContainer(ent.Owner, out var container) || !TryComp<InventoryComponent>(container.Owner, out var inv))
return false;
if (!_inventory.TryGetSlot(container.Owner, container.ID, out var slotDef, inv))
return false;
foreach (var slot in inv.Slots)
{
if (!_inventory.TryGetSlotEntity(container.Owner, slot.Name, out var slotEnt, inv))
continue;
if (_stainBlockerQuery.TryComp(slotEnt, out var blocker) && blocker.BlockedSlots.HasFlag(slotDef.SlotFlags))
return true;
}
return false;
}
public void UpdateVisuals(Entity<StainableComponent> ent)
{
_item.VisualsChanged(ent.Owner);
if (TryComp<AppearanceComponent>(ent.Owner, out var app))
{
var toggled = true;
if (_appearance.TryGetData(ent.Owner, StainVisuals.Toggle, out bool current, app))
toggled = !current;
_appearance.SetData(ent.Owner, StainVisuals.Toggle, toggled, app);
}
if (_container.TryGetContainingContainer(ent.Owner, out var container))
{
if (TryComp<AppearanceComponent>(container.Owner, out var wearerApp))
{
_appearance.QueueUpdate(container.Owner, wearerApp);
Dirty(container.Owner, wearerApp);
}
}
}
private void OnGetVerbs(Entity<StainableComponent> ent, ref GetVerbsEvent<Verb> args)
{
if (!args.CanInteract || !args.CanAccess || args.Using != ent.Owner)
return;
if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var sol) || sol.Volume <= 0)
return;
var user = args.User;
args.Verbs.Add(new Verb
{
Text = Loc.GetString("stain-verb-wring"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/bubbles.svg.192dpi.png")),
Act = () =>
{
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, ent.Comp.WringDoAfterDuration, new WringStainDoAfterEvent(), ent.Owner)
{
BreakOnMove = true,
BreakOnDamage = true,
NeedHand = true
});
}
});
}
private void OnWring(Entity<StainableComponent> ent, ref WringStainDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
args.Handled = true;
if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out var solComp, out var sol))
return;
var split = _solution.SplitSolution(solComp.Value, sol.Volume);
UpdateVisuals(ent);
if (_puddle.TrySpillAt(args.User, split, out _))
_popup.PopupEntity(Loc.GetString("stain-verb-wring-success"), args.User, args.User);
}
}
[Serializable, NetSerializable]
public enum StainVisuals : byte
{
Toggle,
}

View File

@ -0,0 +1,118 @@
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.Shared._Funkystation.WashingMachine;
public abstract class SharedWashingMachineSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = null!;
[Dependency] protected readonly SharedAudioSystem Audio = null!;
[Dependency] private readonly SharedPowerReceiverSystem _power = null!;
[Dependency] protected readonly SharedEntityStorageSystem Storage = null!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = null!;
[Dependency] private readonly SharedPopupSystem _popup = null!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WashingMachineComponent, StorageOpenAttemptEvent>(OnStorageOpenAttempt);
SubscribeLocalEvent<WashingMachineComponent, ActivateInWorldEvent>(OnActivate, before: [typeof(SharedEntityStorageSystem)]);
SubscribeLocalEvent<WashingMachineComponent, GetVerbsEvent<ActivationVerb>>(OnGetVerbs);
}
private void OnStorageOpenAttempt(Entity<WashingMachineComponent> ent, ref StorageOpenAttemptEvent args)
{
if (ent.Comp.State != WashingMachineState.Idle)
args.Cancelled = true;
}
private void OnActivate(Entity<WashingMachineComponent> ent, ref ActivateInWorldEvent args)
{
if (args.Handled || !args.Complex)
return;
if (ent.Comp.State != WashingMachineState.Idle || !_power.IsPowered(ent.Owner) || Storage.IsOpen(ent.Owner))
return;
if (!TryComp<EntityStorageComponent>(ent, out var storage) || storage.Contents.ContainedEntities.Count == 0)
return;
if (Timing.CurTime < ent.Comp.NextWashAllowed)
{
_popup.PopupClient(Loc.GetString("washing-machine-cooldown"), ent.Owner, args.User);
args.Handled = true;
return;
}
args.Handled = true;
TryStartWash(ent, args.User);
}
private void OnGetVerbs(Entity<WashingMachineComponent> ent, ref GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanInteract || !args.CanComplexInteract)
return;
if (ent.Comp.State != WashingMachineState.Idle || !_power.IsPowered(ent.Owner) || Storage.IsOpen(ent.Owner))
return;
if (!TryComp<EntityStorageComponent>(ent, out var storage) || storage.Contents.ContainedEntities.Count == 0)
return;
var user = args.User;
args.Verbs.Add(new ActivationVerb
{
Text = Loc.GetString("washing-machine-start"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/Spare/poweronoff.svg.192dpi.png")),
Act = () =>
{
if (Timing.CurTime < ent.Comp.NextWashAllowed)
{
_popup.PopupClient(Loc.GetString("washing-machine-cooldown"), ent.Owner, user);
return;
}
TryStartWash(ent, user);
}
});
}
protected virtual bool TryStartWash(Entity<WashingMachineComponent> ent, EntityUid user)
{
if (ent.Comp.State != WashingMachineState.Idle || !_power.IsPowered(ent.Owner) || Storage.IsOpen(ent.Owner))
return false;
if (Timing.CurTime < ent.Comp.NextWashAllowed)
return false;
if (!TryComp<EntityStorageComponent>(ent, out var storage) || storage.Contents.ContainedEntities.Count == 0)
return false;
ent.Comp.State = WashingMachineState.Washing;
ent.Comp.WashFinishTime = Timing.CurTime + ent.Comp.WashTime;
ent.Comp.NextWashingStep = Timing.CurTime + ent.Comp.WashingStepCooldown;
Dirty(ent.Owner, ent.Comp);
Appearance.SetData(ent.Owner, WashingMachineVisuals.State, WashingMachineState.Washing);
var items = storage.Contents.ContainedEntities.ToHashSet();
var machineEv = new WashingMachineStartedWashingEvent(items);
RaiseLocalEvent(ent.Owner, machineEv);
var itemEv = new WashingMachineIsBeingWashed(ent.Owner, items);
foreach (var item in items)
{
RaiseLocalEvent(item, itemEv);
}
return true;
}
}

View File

@ -0,0 +1,137 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._Funkystation.WashingMachine;
/// <summary>
/// This defines a machine with entityStorage capable of cleaning reagent stains on clothing.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class WashingMachineComponent : Component
{
/// <summary>
/// The duration of the washing process that determines <see cref="WashFinishTime"/>.
/// </summary>
[DataField]
public TimeSpan WashTime = TimeSpan.FromSeconds(5);
/// <summary>
/// When the washing process is finished.
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan? WashFinishTime;
/// <summary>
/// The cooldown after each washing step for the next one.
/// </summary>
[DataField]
public TimeSpan WashingStepCooldown = TimeSpan.FromSeconds(1);
/// <summary>
/// The next time when washing is calculated (Damaging entities, spraying with water, etc.)
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan? NextWashingStep;
/// <summary>
/// The cooldown length after <see cref="WashFinishTime"/> to determine <see cref="NextWashAllowed"/>.
/// </summary>
[DataField]
public TimeSpan Cooldown = TimeSpan.FromSeconds(6);
/// <summary>
/// The time when the washing machine can wash again after finishing a load of laundry.
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan? NextWashAllowed;
/// <summary>
/// The sound the washing machine makes during the washing process.
/// </summary>
[DataField]
public SoundSpecifier? WashLoopSound;
/// <summary>
/// The sound the washing machine makes after it finishes.
/// </summary>
[DataField]
public SoundSpecifier? WashFinishedSound;
/// <summary>
/// The current State of the washing machine, used for appearance.
/// </summary>
[DataField, AutoNetworkedField]
public WashingMachineState State = WashingMachineState.Idle;
/// <summary>
/// The current audio being played.
/// </summary>
/// <remarks>We save it so we can stop the looping audio when the process finishes.</remarks>
public EntityUid? AudioStream;
/// <summary>
/// The chance of a thump sound to occur whenever something that isn't clothing is washed.
/// </summary>
[DataField]
public float ThumpSoundChance = 0.8f;
/// <summary>
/// The reagent to spray on entities inside the active washing machine.
/// </summary>
[DataField]
public string SprayReagent = "Water";
/// <summary>
/// The amount of reagent to spray on entities inside the active washing machine.
/// </summary>
[DataField]
public float ReagentSprayAmount = 10.0f;
/// <summary>
/// The chance to spray the reagent on entities inside per step.
/// </summary>
[DataField]
public float ReagentSprayChance = 1.0f;
/// <summary>
/// The damage dealt to entities within an active washing machine every <see cref="WashingStepCooldown"/>.
/// </summary>
[DataField]
public DamageSpecifier EntityBluntDamage = new()
{
DamageDict = new Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2>
{
{ "Blunt", 6f },
}
};
/// <summary>
/// The damage done to the washing machine itself upon finishing the washing process, multiplied by <see cref="WashTime"/>.
/// </summary>
[DataField]
public DamageSpecifier SelfDamage = new()
{
DamageDict = new Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2>
{
{ "Blunt", 5f },
}
};
}
[Serializable, NetSerializable]
public enum WashingMachineState : byte
{
Idle,
Washing,
}
[Serializable, NetSerializable]
public enum WashingMachineVisuals : byte
{
State
}

View File

@ -0,0 +1,45 @@
namespace Content.Shared._Funkystation.WashingMachine;
public sealed class WashingMachineIsBeingWashed : EntityEventArgs
{
public EntityUid WashingMachine;
public HashSet<EntityUid> Items;
public WashingMachineIsBeingWashed(EntityUid washingMachine, HashSet<EntityUid> items)
{
WashingMachine = washingMachine;
Items = items;
}
}
public sealed class WashingMachineStartedWashingEvent : EntityEventArgs
{
public HashSet<EntityUid> Items;
public WashingMachineStartedWashingEvent(HashSet<EntityUid> items)
{
Items = items;
}
}
public sealed class WashingMachineWashedEvent : EntityEventArgs
{
public EntityUid WashingMachine;
public HashSet<EntityUid> Items;
public WashingMachineWashedEvent(EntityUid washingMachine, HashSet<EntityUid> items)
{
WashingMachine = washingMachine;
Items = items;
}
}
public sealed class WashingMachineFinishedWashingEvent : EntityEventArgs
{
public HashSet<EntityUid> Items;
public WashingMachineFinishedWashingEvent(HashSet<EntityUid> items)
{
Items = items;
}
}

View File

@ -0,0 +1,2 @@
washing-machine-start = Start washing machine
washing-machine-cooldown = The tank is still draining.

View File

@ -0,0 +1,2 @@
stain-verb-wring = Wring out clothes
stain-verb-wring-success = You wring out the cloth, spilling liquid all over the floor.

View File

@ -250,4 +250,8 @@ BoxPerformer: BoxPerformerDV
# 2026-07-04
SpawnPointMedicalBorg: SpawnPointBorg
SpawnPointSecurityBorg: SpawnPointBorg
SpawnPointSecurityBorg: SpawnPointBorg
# 2026-07-19
WashingMachineBroken: WashingMachine
WashingMachineFilledClothes: WashingMachine

View File

@ -19,6 +19,8 @@
reagents:
- ReagentId: Fiber
Quantity: 10
stain: # Funky - Stainable Clothing.
maxVol: 5
- type: Tag
tags:
- ClothMade
@ -27,6 +29,17 @@
damageProtection:
flatReductions:
Heat: 5 # the average lightbulb only does around four damage!
# Funky Start - Stainable Clothing.
- type: Appearance
- type: Stainable
clothingVisuals:
gloves:
- sprite: _Funkystation/Effects/blood.rsi
state: gloveblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: glovebloodicon
# Funky End - Stainable Clothing.
- type: entity
abstract: true

View File

@ -20,10 +20,23 @@
reagents:
- ReagentId: Fiber
Quantity: 10
stain: # Funky - Clothing stains.
maxVol: 5
- type: Tag
tags:
- ClothMade
- WhitelistChameleon
# Funky Start - Clothing stains.
- type: Appearance
- type: Stainable
clothingVisuals:
head:
- sprite: _Funkystation/Effects/blood.rsi
state: helmetblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: helmetbloodicon
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -141,6 +154,8 @@
- Snout
- HeadTop
- HeadSide
- type: StainBlocker # Funky - Clothing stains.
slots: [MASK]
- type: entity
abstract: true
@ -189,6 +204,19 @@
- Snout
- HeadTop
- HeadSide
# Funky Start - Clothing stains.
- type: Appearance
- type: Stainable
clothingVisuals:
head:
- sprite: _Funkystation/Effects/blood.rsi
state: helmetblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: helmetbloodicon
- type: StainBlocker
slots: [MASK]
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -276,3 +304,14 @@
- Hair
- HeadTop
- HeadSide
# Funky Start - Clothing stains.
- type: Appearance
- type: Stainable
clothingVisuals:
head:
- sprite: _Funkystation/Effects/blood.rsi
state: helmetblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: helmetbloodicon
# Funky End - Clothing stains.

View File

@ -11,6 +11,17 @@
slots: [mask]
- type: StaticPrice
price: 25
# Funky Start - Stainable Clothing.
- type: Appearance
- type: Stainable
clothingVisuals:
mask:
- sprite: _Funkystation/Effects/blood.rsi
state: maskblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: maskbloodicon
# Funky End - Stainable Clothing.
- type: entity
abstract: true
@ -53,6 +64,8 @@
reagents:
- ReagentId: Fiber
Quantity: 10
stain: # Funky - Stainable Clothing
maxVol: 5
- type: Tag
tags:
- ClothMade

View File

@ -25,6 +25,16 @@
damageCoefficient: 0.75 # Decent at stopping disablers
- type: ExplosionResistance
damageCoefficient: 0.90
# Funky Start - Stainable Clothing.
- type: Stainable
clothingVisuals:
outerClothing:
- sprite: _Funkystation/Effects/blood.rsi
state: armorblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: armorbloodicon
# Funky End - Stainable Clothing.
#Standard armor vest, allowed for security and bartenders
- type: entity

View File

@ -9,6 +9,28 @@
- type: Sprite
state: icon
- type: AllowSuitStorage # DeltaV - allow suit storage for all outer clothing, no whitelist
# Funky Start - Clothing stains.
- type: Appearance
- type: Stainable
clothingVisuals:
outerClothing:
- sprite: _Funkystation/Effects/blood.rsi
state: outerclothing
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: outerclothing
- type: SolutionContainerManager
solutions:
food:
maxVol: 30
reagents:
- ReagentId: Fiber
Quantity: 30
stain:
maxVol: 5
- type: StainBlocker
slots: [INNERCLOTHING]
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -27,6 +49,18 @@
- type: Tag
tags:
- WhitelistChameleon
# Funky Start - Clothing stains.
- type: Stainable
clothingVisuals:
outerClothing:
- sprite: _Funkystation/Effects/blood.rsi
state: suitblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: suitbloodicon
- type: StainBlocker
slots: [INNERCLOTHING]
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -46,6 +80,16 @@
type: StorageBoundUserInterface
- type: StaticPrice
price: 70
# Funky Start - Clothing stains.
- type: Stainable
clothingVisuals:
outerClothing:
- sprite: _Funkystation/Effects/blood.rsi
state: coatblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: coatbloodicon
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -67,6 +111,16 @@
- state: icon-open
map: ["foldedLayer"]
visible: false
# Funky Start - Clothing stains.
- type: Stainable
clothingVisuals:
outerClothing:
- sprite: _Funkystation/Effects/blood.rsi
state: outerclothing
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: outerclothing
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -159,6 +213,8 @@
- type: CosmicTransmutable # DeltaV
transmutesTo: ClothingOuterHardsuitCosmicCult
requiredGlyphType: CosmicGlyphWarding
- type: StainBlocker # Funky - Clothing stains.
slots: [INNERCLOTHING, FEET, GLOVES]
- type: entity
abstract: true
@ -181,9 +237,9 @@
- type: HeldSpeedModifier
- type: Item
size: Huge
- type: Tag
- type: Tag # DeltaV - Harpy Wings with Hardsuit
tags:
- HidesHarpyWings # DeltaV: Used by harpies to help render their hardsuit sprites
- HidesHarpyWings
- type: ProtectedFromStepTriggers
slots: WITHOUT_POCKET
- type: DamageOnInteractProtection
@ -197,6 +253,8 @@
- type: CosmicTransmutable # DeltaV
transmutesTo: ClothingOuterHardsuitCosmicCult
requiredGlyphType: CosmicGlyphWarding
- type: StainBlocker # Funky - Clothing stains.
slots: [INNERCLOTHING, FEET, GLOVES]
- type: entity
parent: ClothingOuterBase

View File

@ -30,6 +30,8 @@
reagents:
- ReagentId: Fiber
Quantity: 30
stain: # Funky - Stainable Clothing.
maxVol: 5
- type: Tag
tags:
- ClothMade
@ -486,7 +488,7 @@
sprite: _DV/Clothing/OuterClothing/WinterCoats/medical.rsi # DeltaV - Recolor to match department color
# clothingVisuals:
# outerClothing:
# - state: MED-equipped-OUTERCLOTHING
# - state: MED-equipped-OUTERCLOTHING
- type: Armor
modifiers:
coefficients:

View File

@ -19,12 +19,25 @@
reagents:
- ReagentId: Fiber
Quantity: 10
stain: # Funky - Stainable Clothing.
maxVol: 5
- type: Tag
tags:
- ClothMade
- Recyclable
- WhitelistChameleon
- type: ProtectedFromStepTriggers
# Funky Start - Stainable Clothing.
- type: Appearance
- type: Stainable
clothingVisuals:
shoes:
- sprite: _Funkystation/Effects/blood.rsi
state: shoeblood
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: shoebloodicon
# Funky End - Stainable Clothing
- type: entity
abstract: true

View File

@ -26,11 +26,31 @@
reagents:
- ReagentId: Fiber
Quantity: 30
stain: # Funky - Clothing stains.
maxVol: 5
- type: Tag
tags:
- ClothMade
- Recyclable
- WhitelistChameleon
# Funky Start - Clothing stains.
- type: Appearance
- type: Stainable
clothingVisuals:
jumpsuit:
- sprite: _Funkystation/Effects/blood.rsi
state: uniformblood
itemVisuals:
left:
- sprite: Effects/Stains/jumpsuit.rsi
state: inhand-right
right:
- sprite: Effects/Stains/jumpsuit.rsi
state: inhand-left
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: uniformbloodicon
# Funky End - Clothing stains.
- type: entity
abstract: true
@ -45,6 +65,23 @@
- Skirt # Delta-V : Harpies can wear this
- ClothMade # Delta-V : Moths can eat this
- WhitelistChameleon # Delta-V : You can set Chameleon clothes to this.
# Funky Start - Clothing stains.
- type: Stainable
clothingVisuals:
jumpsuit:
- sprite: _Funkystation/Effects/blood.rsi
state: uniformblood
itemVisuals:
left:
- sprite: Effects/Stains/jumpskirt.rsi
state: inhand-right
right:
- sprite: Effects/Stains/jumppskirt.rsi
state: inhand-left
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: uniformbloodicon
# Funky End - Clothing stains.
- type: entity
@ -74,3 +111,20 @@
- Skirt # Delta-V : Harpies can wear this
- ClothMade # Delta-V : Moths can eat this
- WhitelistChameleon # Delta-V : You can set Chameleon clothes to this.
# Funky Start - Clothing stains.
- type: Stainable
clothingVisuals:
jumpsuit:
- sprite: _Funkystation/Effects/blood.rsi
state: uniformblood
itemVisuals:
left:
- sprite: Effects/Stains/jumpskirt.rsi
state: inhand-right
right:
- sprite: Effects/Stains/jumpskirt.rsi
state: inhand-left
iconVisuals:
- sprite: _Funkystation/Effects/blood.rsi
state: uniformbloodicon
# Funky End - Clothing stains.

View File

@ -215,7 +215,21 @@
- type: Tag
tags:
- DNASolutionScannable
- type: PuddleFootPrints # DeltaV- Begin updates from EE Blood Puddle
# DeltaV Start.
- type: PuddleFootPrints # Begin updates from EE Blood Puddle
# Clothing Stains
- type: TriggerOnStepTrigger
keyOut: spill
- type: TriggerOnTimedCollide
keyOut: spill
- type: SpillOnTrigger
keysIn:
- spill
targetUser: true
solution: puddle
targetSlots: FEET
proneTargetSlots: WITHOUT_POCKET
# DeltaV End - Clothing Stains.
- type: entity
name: footstep

View File

@ -1,206 +0,0 @@
- type: entity
id: WashingMachine
parent: BaseStructureDynamic
name: washing machine
description: A machine that washes clothes with a spinning steel drum in a shiny frame.
components:
- type: Sprite
noRot: true
sprite: Nyanotrasen/Structures/Machines/washer.rsi
layers:
# GenericVisualizer doesn't have a way to deal with multiple conditions at
# once. So have all the layers ready to go, but don't show them unless
# they're relevant.
#
# If we actually get more complex interactions with washing machines later,
# we can take a look at making this cleaner.
- map: ["enum.WashingMachineVisualLayers.Normal"]
state: "normal-base"
- map: ["enum.WashingMachineVisualLayers.Broken"]
state: "broken-base"
visible: false
- map: ["enum.WashingMachineVisualLayers.Contents"]
state: "stuff"
visible: false
- map: ["enum.WashingMachineVisualLayers.NormalDoor"]
state: "normal-closed"
- map: ["enum.WashingMachineVisualLayers.BrokenDoor"]
state: "broken-closed"
visible: false
- type: Appearance
- type: GenericVisualizer
visuals:
enum.WashingMachineVisualState.Broken:
enum.WashingMachineVisualLayers.Normal:
True: { visible: false }
False: { visible: true }
enum.WashingMachineVisualLayers.NormalDoor:
True: { visible: false }
False: { visible: true }
enum.WashingMachineVisualLayers.Broken:
True: { visible: true }
False: { visible: false }
enum.WashingMachineVisualLayers.BrokenDoor:
True: { visible: true }
False: { visible: false }
enum.StorageVisuals.HasContents:
enum.WashingMachineVisualLayers.Contents:
True: { visible: true }
False: { visible: false }
enum.StorageVisuals.Open:
enum.WashingMachineVisualLayers.NormalDoor:
True: { state: "normal-open" }
False: { state: "normal-closed" }
enum.WashingMachineVisualLayers.BrokenDoor:
True: { state: "broken-open" }
False: { state: "broken-closed" }
- type: WashingMachine
- type: InteractionOutline
- type: Physics
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.35,-0.25,0.35,0.49"
density: 600
mask:
- MachineMask
layer:
- MachineLayer
- type: Climbable
delay: 1.6
- type: Storage
maxItemSize: Huge # DeltaV - changed to huge (so felinids can fit)
grid:
- 0,0,5,5
storageOpenSound:
path: /Audio/Nyanotrasen/Machines/washer_open.ogg
storageCloseSound:
path: /Audio/Nyanotrasen/Machines/washer_close.ogg
- type: AllowsSleepInside # DeltaV - let felinids sleep inside washing machines
- type: ContainerContainer
containers:
storagebase: !type:Container
ents: []
- type: UserInterface
interfaces:
enum.StorageUiKey.Key:
type: StorageBoundUserInterface
- type: UseDelay
delay: 0.5
- type: Repairable
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 200
behaviors:
- !type:DoActsBehavior
acts: ["Destruction"]
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:PlaySoundBehavior
sound:
collection: MetalBreak
- !type:SpawnEntitiesBehavior
spawn:
SheetSteel1:
min: 1
max: 1
- !type:DoActsBehavior
acts: ["Destruction"]
- trigger:
!type:DamageTrigger
damage: 50
behaviors:
- !type:PlaySoundBehavior
sound:
collection: GlassBreak
- !type:EmptyAllContainersBehaviour
- !type:SpawnEntitiesBehavior
spawn:
ShardGlass:
min: 1
max: 1
- !type:DoActsBehavior
acts: ["Breakage"]
- type: entity
id: WashingMachineBroken
parent: WashingMachine
name: washing machine
suffix: broken
description: A shattered mess of glass and steel that won't be washing anything anytime soon. It looks dusty.
components:
- type: Sprite
layers:
- map: ["enum.WashingMachineVisualLayers.Broken"]
state: "broken-base"
- map: ["enum.WashingMachineVisualLayers.Contents"]
state: "stuff"
visible: false
- map: ["enum.WashingMachineVisualLayers.BrokenDoor"]
state: "broken-closed"
- type: Appearance
- type: GenericVisualizer
visuals:
enum.StorageVisuals.HasContents:
enum.WashingMachineVisualLayers.Contents:
True: { visible: true }
False: { visible: false }
enum.StorageVisuals.Open:
enum.WashingMachineVisualLayers.BrokenDoor:
True: { state: "broken-open" }
False: { state: "broken-closed" }
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 150
behaviors:
- !type:DoActsBehavior
acts: ["Destruction"]
- trigger:
!type:DamageTrigger
damage: 50
behaviors:
- !type:PlaySoundBehavior
sound:
collection: MetalBreak
- !type:SpawnEntitiesBehavior
spawn:
SheetSteel1:
min: 1
max: 1
- !type:DoActsBehavior
acts: ["Destruction"]
- type: entity
id: WashingMachineFilledClothes
parent: WashingMachine
name: washing machine
suffix: random clothes
components:
- type: EntityTableContainerFill
containers:
storagebase: !type:AllSelector
children:
- id: Soap
prob: 0.3
- id: ClothingOuterWinterCoatPlaid
prob: 0.5
- id: ClothingUniformMNKTracksuitBlack
prob: 0.3
- id: ClothingCostumeNaota
prob: 0.2
- id: ClothingHeadBandSkull
prob: 0.2
- id: ClothingNeckScarfStripedBlue
prob: 0.3

View File

@ -51,6 +51,7 @@
- WeaponSprayNozzle
- ClothingBackpackWaterTank
- MegaSprayBottle
- WashingMachineCircuitboard # Funky
- type: latheRecipePack
id: Instruments

View File

@ -168,6 +168,7 @@
- MegaSprayBottle
- BorgModuleAdvancedCleaning
- TrashBagOfHolding # DeltaV
- WashingMachineCircuitboard # Funky
# Begin DeltaV Removals - waste of points nobody makes, moved into civilian mechs
#- type: technology

View File

@ -0,0 +1,9 @@
- type: cargoProduct
id: ServiceWashingMachineKit
icon:
sprite: _Funkystation/Structures/Machines/washing_machine.rsi
state: base
product: CrateServiceWashingMachineSet
cost: 1500
category: cargoproduct-category-name-service
group: market

View File

@ -0,0 +1,13 @@
- type: entity
id: CrateServiceWashingMachineSet
parent: CratePlastic
name: DIY washing machine kit
description: A Nanotrasen Commercial Model-C washing machine, disassembled and ready for shipping. Contains small parts that may be ingested by infants.
components:
- type: StorageFill
contents:
- id: SheetSteel1
amount: 6
- id: CableApcStack10
- id: MicroManipulatorStockPart
- id: WashingMachineCircuitboard

View File

@ -0,0 +1,12 @@
- type: entity
parent: BaseMachineCircuitboard
id: WashingMachineCircuitboard
name: Nanotrasen Commercial Model-C washing machine board
description: A machine printed circuit board for an industrial-grade washing machine.
components:
- type: MachineBoard
prototype: WashingMachine
stackRequirements:
Steel: 1
Manipulator: 1
Cable: 1

View File

@ -0,0 +1,8 @@
- type: entity
parent: BaseFlatpack
id: FlatpackWashingMachine
name: Nanotrasen Commercial Model-C washing machine flatpack
description: An industrial-grade washing machine, mechanically compressed into a small flatpack.
components:
- type: Flatpack
entity: WashingMachine

View File

@ -0,0 +1,83 @@
- type: entity
id: WashingMachine
parent: [ BaseMachinePowered, ConstructibleMachine ]
name: Nanotrasen Commercial Model-C washing machine
description: An industrial-grade washing machine designed to clean even the grimiest of uniforms. Keep hands, hard objects and small crewmembers away from drum during cycle.
placement:
mode: SnapgridCenter
components:
- type: Sprite
sprite: _Funkystation/Structures/Machines/washing_machine.rsi
snapCardinals: true
layers:
- state: base
map: [ "enum.StorageVisualLayers.Base" ]
- state: empty
map: [ "content" ]
- state: door-closed
map: [ "enum.StorageVisualLayers.Door" ]
- state: running
map: [ "washing" ]
visible: false
- type: WashingMachine
washLoopSound:
path: /Audio/_Funkystation/Machines/washing_loop.ogg
params:
loop: true
washFinishedSound:
path: /Audio/_Funkystation/Machines/washing_open.ogg
- type: EntityStorage
isCollidableWhenOpen: false
open: false
capacity: 4
- type: Forensics
canDnaBeCleaned: false
- type: PlaceableSurface
isPlaceable: false
- type: Appearance
- type: GenericVisualizer
visuals:
enum.StorageVisuals.Open:
content:
True: { visible: false }
False: { visible: true }
enum.StorageVisuals.HasContents:
content:
True: { state: full }
False: { state: empty }
enum.WashingMachineVisuals.State:
washing:
Idle: { visible: false }
Washing: { visible: true }
Broken: { visible: false }
- type: EntityStorageVisuals
stateBaseClosed: base
stateBaseOpen: base
stateDoorOpen: door-open
stateDoorClosed: door-closed
- type: Machine
board: WashingMachineCircuitboard
- type: ContainerContainer
containers:
machine_board: !type:Container
machine_parts: !type:Container
entity_storage: !type:Container
- type: Construction
forbiddenContainers:
- entity_storage
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 50
behaviors:
- !type:PlaySoundBehavior
sound:
collection: MetalBreak
- !type:ChangeConstructionNodeBehavior
node: machineFrame
- !type:DoActsBehavior
acts: ["Destruction"]

View File

@ -0,0 +1,7 @@
## Service
# Washing machine
- type: latheRecipe
parent: BaseCircuitboardRecipe
id: WashingMachineCircuitboard
result: WashingMachineCircuitboard

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,74 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/master/icons/effects/blood.dmi and modified by Will-Oliver-Br",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "uniformblood",
"directions": 4
},
{
"name": "armorblood",
"directions": 4
},
{
"name": "helmetblood",
"directions": 4
},
{
"name": "suitblood",
"directions": 4
},
{
"name": "maskblood",
"directions": 4
},
{
"name": "shoeblood",
"directions": 4
},
{
"name": "coatblood",
"directions": 4
},
{
"name": "gloveblood",
"directions": 4
},
{
"name": "outerclothing",
"directions": 4
},
{
"name": "itemblood"
},
{
"name": "glovebloodicon"
},
{
"name": "coatbloodicon"
},
{
"name": "shoebloodicon"
},
{
"name": "maskbloodicon"
},
{
"name": "suitbloodicon"
},
{
"name": "helmetbloodicon"
},
{
"name": "armorbloodicon"
},
{
"name": "uniformbloodicon"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

View File

@ -0,0 +1,43 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "By AraiMaia for Funky Station",
"size":
{
"x": 32,
"y": 32
},
"states":
[{
"name": "base"
},
{
"name": "empty"
},
{
"name": "door-open"
},
{
"name": "door-closed"
},
{
"name": "full"
},
{
"name": "panel"
},
{
"name": "running",
"delays":
[
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB