Add more DNA interactions (#21989)

* Add more DNA interactions

* remove unused import

* update based on feedback

* Add event for chemistrysystem.injector

* move event to shared; transfer dna to implanter

* doafter and interaction event fixes

* add BreakOnHandChange

* doh

* use events instead of updating component directly

* Add DataFields to ForensicScannerComponent fields

* Convert most events to system api call
This commit is contained in:
themias
2023-12-15 04:52:55 -05:00
committed by GitHub
parent 2455980090
commit 9cc4a50692
20 changed files with 291 additions and 26 deletions

View File

@@ -58,6 +58,12 @@ namespace Content.Client.Forensics
{
text.AppendLine(dna);
}
text.AppendLine();
text.AppendLine(Loc.GetString("forensic-scanner-interface-residues"));
foreach (var residue in msg.Residues)
{
text.AppendLine(residue);
}
Diagnostics.Text = text.ToString();
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.ReactionEffects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.HealthExaminable;
using Content.Server.Popups;
using Content.Shared.Alert;
@@ -22,6 +21,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Speech.EntitySystems;
using Robust.Server.Audio;
using Robust.Shared.GameObjects;
using Content.Server.Forensics;
namespace Content.Server.Body.Systems;
@@ -38,6 +39,7 @@ public sealed class BloodstreamSystem : EntitySystem
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
public override void Initialize()
{
@@ -322,11 +324,7 @@ public sealed class BloodstreamSystem : EntitySystem
component.BloodTemporarySolution.AddSolution(temp, _prototypeManager);
if (_puddleSystem.TrySpillAt(uid, component.BloodTemporarySolution, out var puddleUid, false))
{
if (TryComp<DnaComponent>(uid, out var dna))
{
var comp = EnsureComp<ForensicsComponent>(puddleUid);
comp.DNAs.Add(dna.DNA);
}
_forensicsSystem.TransferDna(puddleUid, uid, false);
}
component.BloodTemporarySolution.RemoveAllSolution();
@@ -378,11 +376,7 @@ public sealed class BloodstreamSystem : EntitySystem
if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
{
if (TryComp<DnaComponent>(uid, out var dna))
{
var comp = EnsureComp<ForensicsComponent>(puddleUid);
comp.DNAs.Add(dna.DNA);
}
_forensicsSystem.TransferDna(puddleUid, uid, false);
}
}

View File

@@ -15,6 +15,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Verbs;
using Content.Shared.Stacks;
using Robust.Shared.Player;
using Content.Shared.Forensics;
namespace Content.Server.Chemistry.EntitySystems;
@@ -290,7 +291,7 @@ public sealed partial class ChemistrySystem
("target", Identity.Entity(target, EntityManager))), injector, user);
Dirty(component);
AfterInject(component, injector);
AfterInject(component, injector, target);
}
private void TryInject(InjectorComponent component, EntityUid injector, EntityUid targetEntity, Solution targetSolution, EntityUid user, bool asRefill)
@@ -328,10 +329,10 @@ public sealed partial class ChemistrySystem
("target", Identity.Entity(targetEntity, EntityManager))), injector, user);
Dirty(component);
AfterInject(component, injector);
AfterInject(component, injector, targetEntity);
}
private void AfterInject(InjectorComponent component, EntityUid injector)
private void AfterInject(InjectorComponent component, EntityUid injector, EntityUid target)
{
// Automatically set syringe to draw after completely draining it.
if (_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution)
@@ -339,9 +340,13 @@ public sealed partial class ChemistrySystem
{
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
}
// Leave some DNA from the injectee on it
var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
RaiseLocalEvent(target, ref ev);
}
private void AfterDraw(InjectorComponent component, EntityUid injector)
private void AfterDraw(InjectorComponent component, EntityUid injector, EntityUid target)
{
// Automatically set syringe to inject after completely filling it.
if (_solutions.TryGetSolution(injector, InjectorComponent.SolutionName, out var solution)
@@ -349,6 +354,10 @@ public sealed partial class ChemistrySystem
{
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
}
// Leave some DNA from the drawee on it
var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
RaiseLocalEvent(target, ref ev);
}
private void TryDraw(InjectorComponent component, EntityUid injector, EntityUid targetEntity, Solution targetSolution, EntityUid user, BloodstreamComponent? stream = null)
@@ -389,7 +398,7 @@ public sealed partial class ChemistrySystem
("target", Identity.Entity(targetEntity, EntityManager))), injector, user);
Dirty(component);
AfterDraw(component, injector);
AfterDraw(component, injector, targetEntity);
}
private void DrawFromBlood(EntityUid user, EntityUid injector, EntityUid target, InjectorComponent component, Solution injectorSolution, BloodstreamComponent stream, FixedPoint2 transferAmount)
@@ -414,7 +423,7 @@ public sealed partial class ChemistrySystem
("target", Identity.Entity(target, EntityManager))), injector, user);
Dirty(component);
AfterDraw(component, injector);
AfterDraw(component, injector, target);
}
}

View File

@@ -14,6 +14,7 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Timing;
using Robust.Shared.GameStates;
using Content.Shared.Forensics;
namespace Content.Server.Chemistry.EntitySystems
{
@@ -138,6 +139,9 @@ namespace Content.Server.Chemistry.EntitySystems
_reactiveSystem.DoEntityReaction(target.Value, removedSolution, ReactionMethod.Injection);
_solutions.TryAddSolution(target.Value, targetSolution, removedSolution);
var ev = new TransferDnaEvent { Donor = target.Value, Recipient = uid };
RaiseLocalEvent(target.Value, ref ev);
// same LogType as syringes...
_adminLogger.Add(LogType.ForceFeed, $"{_entMan.ToPrettyString(user):user} injected {_entMan.ToPrettyString(target.Value):target} with a solution {SolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {_entMan.ToPrettyString(uid):using}");

View File

@@ -13,21 +13,27 @@ namespace Content.Server.Forensics
/// <summary>
/// A list of fingerprint GUIDs that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[ViewVariables(VVAccess.ReadOnly), DataField("fingerprints")]
public List<string> Fingerprints = new();
/// <summary>
/// A list of glove fibers that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[ViewVariables(VVAccess.ReadOnly), DataField("fibers")]
public List<string> Fibers = new();
/// <summary>
/// DNA that the forensic scanner found from the <see cref="DNAComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[ViewVariables(VVAccess.ReadOnly), DataField("dnas")]
public List<string> DNAs = new();
/// <summary>
/// Residue that the forensic scanner found from the <see cref="ForensicsComponent"/> on an entity.
/// </summary>
[ViewVariables(VVAccess.ReadOnly), DataField("residues")]
public List<string> Residues = new();
/// <summary>
/// What is the name of the entity that was scanned last?
/// </summary>

View File

@@ -11,5 +11,27 @@ namespace Content.Server.Forensics
[DataField("dnas")]
public HashSet<string> DNAs = new();
[DataField("residues")]
public HashSet<string> Residues = new();
/// <summary>
/// How long it takes to wipe the prints/blood/etc. off of this entity
/// </summary>
[DataField("cleanDelay")]
public float CleanDelay = 12.0f;
/// <summary>
/// How close you must be to wipe the prints/blood/etc. off of this entity
/// </summary>
[DataField("cleanDistance")]
public float CleanDistance = 1.5f;
/// <summary>
/// Can the DNA be cleaned off of this entity?
/// e.g. you can clean the DNA off of a knife, but not a puddle
/// </summary>
[DataField("canDnaBeCleaned")]
public bool CanDnaBeCleaned = true;
}
}

View File

@@ -0,0 +1,15 @@
namespace Content.Server.Forensics;
/// <summary>
/// This controls residues left on items
/// which the forensics system uses.
/// </summary>
[RegisterComponent]
public sealed partial class ResidueComponent : Component
{
[DataField]
public LocId ResidueAdjective = "residue-unknown";
[DataField]
public string? ResidueColor;
}

View File

@@ -51,6 +51,7 @@ namespace Content.Server.Forensics
component.Fingerprints,
component.Fibers,
component.DNAs,
component.Residues,
component.LastScannedName,
component.PrintCooldown,
component.PrintReadyAt);
@@ -74,6 +75,7 @@ namespace Content.Server.Forensics
scanner.Fingerprints = new();
scanner.Fibers = new();
scanner.DNAs = new();
scanner.Residues = new();
}
else
@@ -81,6 +83,7 @@ namespace Content.Server.Forensics
scanner.Fingerprints = forensics.Fingerprints.ToList();
scanner.Fibers = forensics.Fibers.ToList();
scanner.DNAs = forensics.DNAs.ToList();
scanner.Residues = forensics.Residues.ToList();
}
scanner.LastScannedName = MetaData(args.Args.Target.Value).EntityName;
@@ -222,6 +225,12 @@ namespace Content.Server.Forensics
{
text.AppendLine(dna);
}
text.AppendLine();
text.AppendLine(Loc.GetString("forensic-scanner-interface-residues"));
foreach (var residue in component.Residues)
{
text.AppendLine(residue);
}
_paperSystem.SetContent(printed, text.ToString());
_audioSystem.PlayPvs(component.SoundPrint, uid,

View File

@@ -1,5 +1,13 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.DoAfter;
using Content.Shared.DoAfter;
using Content.Shared.Forensics;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Tag;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Random;
namespace Content.Server.Forensics
@@ -8,11 +16,19 @@ namespace Content.Server.Forensics
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
SubscribeLocalEvent<DnaComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<ForensicsComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<ForensicsComponent, CleanForensicsDoAfterEvent>(OnCleanForensicsDoAfter);
SubscribeLocalEvent<DnaComponent, TransferDnaEvent>(OnTransferDnaEvent);
}
private void OnInteract(EntityUid uid, FingerprintComponent component, ContactInteractionEvent args)
@@ -30,6 +46,79 @@ namespace Content.Server.Forensics
component.DNA = GenerateDNA();
}
private void OnBeingGibbed(EntityUid uid, DnaComponent component, BeingGibbedEvent args)
{
foreach(EntityUid part in args.GibbedParts)
{
var partComp = EnsureComp<ForensicsComponent>(part);
partComp.DNAs.Add(component.DNA);
partComp.CanDnaBeCleaned = false;
}
}
private void OnMeleeHit(EntityUid uid, ForensicsComponent component, MeleeHitEvent args)
{
if((args.BaseDamage.DamageDict.TryGetValue("Blunt", out var bluntDamage) && bluntDamage.Value > 0) ||
(args.BaseDamage.DamageDict.TryGetValue("Slash", out var slashDamage) && slashDamage.Value > 0) ||
(args.BaseDamage.DamageDict.TryGetValue("Piercing", out var pierceDamage) && pierceDamage.Value > 0))
{
foreach(EntityUid hitEntity in args.HitEntities)
{
if(TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
component.DNAs.Add(hitEntityComp.DNA);
}
}
}
private void OnAfterInteract(EntityUid uid, ForensicsComponent component, AfterInteractEvent args)
{
if (args.Handled)
return;
if (!_tagSystem.HasTag(args.Used, "CleansForensics"))
return;
if((component.DNAs.Count > 0 && component.CanDnaBeCleaned) || (component.Fingerprints.Count + component.Fibers.Count > 0))
{
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.CleanDelay, new CleanForensicsDoAfterEvent(), uid, target: args.Target, used: args.Used)
{
BreakOnHandChange = true,
NeedHand = true,
BreakOnDamage = true,
BreakOnTargetMove = true,
MovementThreshold = 0.01f,
DistanceThreshold = component.CleanDistance,
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
}
private void OnCleanForensicsDoAfter(EntityUid uid, ForensicsComponent component, CleanForensicsDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
if (!TryComp<ForensicsComponent>(args.Target, out var targetComp))
return;
targetComp.Fibers = new();
targetComp.Fingerprints = new();
if (targetComp.CanDnaBeCleaned)
targetComp.DNAs = new();
// leave behind evidence it was cleaned
if (TryComp<FiberComponent>(args.Used, out var fiber))
targetComp.Fibers.Add(string.IsNullOrEmpty(fiber.FiberColor) ? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial)) : Loc.GetString("forensic-fibers-colored", ("color", fiber.FiberColor), ("material", fiber.FiberMaterial)));
if (TryComp<ResidueComponent>(args.Used, out var residue))
targetComp.Residues.Add(string.IsNullOrEmpty(residue.ResidueColor) ? Loc.GetString("forensic-residue", ("adjective", residue.ResidueAdjective)) : Loc.GetString("forensic-residue-colored", ("color", residue.ResidueColor), ("adjective", residue.ResidueAdjective)));
}
public string GenerateFingerprint()
{
var fingerprint = new byte[16];
@@ -64,5 +153,31 @@ namespace Content.Server.Forensics
if (TryComp<FingerprintComponent>(user, out var fingerprint))
component.Fingerprints.Add(fingerprint.Fingerprint ?? "");
}
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
{
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
}
#region Public API
/// <summary>
/// Transfer DNA from one entity onto the forensics of another
/// </summary>
/// <param name="recipient">The entity receiving the DNA</param>
/// <param name="donor">The entity applying its DNA</param>
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
if (TryComp<DnaComponent>(donor, out var donorComp))
{
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);
}
}
#endregion
}
}

View File

@@ -28,6 +28,7 @@ namespace Content.Server.Medical
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly ThirstSystem _thirst = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
/// <summary>
/// Make an entity vomit, if they have a stomach.
@@ -85,9 +86,7 @@ namespace Content.Server.Medical
if (_puddle.TrySpillAt(uid, solution, out var puddle, false))
{
var forensics = EnsureComp<ForensicsComponent>(puddle);
if (TryComp<DnaComponent>(uid, out var dna))
forensics.DNAs.Add(dna.DNA);
_forensics.TransferDna(puddle, uid, false);
}
// Force sound to play as spill doesn't work if solution is empty.

View File

@@ -54,6 +54,7 @@ public sealed class DrinkSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly StomachSystem _stomach = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
public override void Initialize()
{
@@ -399,9 +400,7 @@ public sealed class DrinkSystem : EntitySystem
//TODO: Grab the stomach UIDs somehow without using Owner
_stomach.TryTransferSolution(firstStomach.Value.Comp.Owner, drained, firstStomach.Value.Comp);
var comp = EnsureComp<ForensicsComponent>(uid);
if (TryComp<DnaComponent>(args.Target, out var dna))
comp.DNAs.Add(dna.DNA);
_forensics.TransferDna(uid, args.Target.Value);
if (!forceDrink && solution.Volume > 0)
args.Repeat = true;

View File

@@ -15,6 +15,8 @@ using Content.Shared.Temperature;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using System.Linq;
using Content.Shared.Inventory.Events;
using Content.Server.Forensics;
namespace Content.Server.Nutrition.EntitySystems
{
@@ -30,6 +32,7 @@ namespace Content.Server.Nutrition.EntitySystems
[Dependency] private readonly SharedItemSystem _items = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
private const float UpdateTimer = 3f;
@@ -44,6 +47,7 @@ namespace Content.Server.Nutrition.EntitySystems
{
SubscribeLocalEvent<SmokableComponent, IsHotEvent>(OnSmokableIsHotEvent);
SubscribeLocalEvent<SmokableComponent, ComponentShutdown>(OnSmokableShutdownEvent);
SubscribeLocalEvent<SmokableComponent, GotEquippedEvent>(OnSmokeableEquipEvent);
InitializeCigars();
InitializePipes();
@@ -85,6 +89,14 @@ namespace Content.Server.Nutrition.EntitySystems
_active.Remove(uid);
}
private void OnSmokeableEquipEvent(EntityUid uid, SmokableComponent component, GotEquippedEvent args)
{
if (args.Slot == "mask")
{
_forensics.TransferDna(uid, args.Equipee, false);
}
}
public override void Update(float frameTime)
{
_timer += frameTime;

View File

@@ -24,3 +24,30 @@ public sealed partial class ForensicPadDoAfterEvent : DoAfterEvent
public override DoAfterEvent Clone() => this;
}
[Serializable, NetSerializable]
public sealed partial class CleanForensicsDoAfterEvent : SimpleDoAfterEvent
{
}
/// <summary>
/// An event to apply DNA evidence from a donor onto some recipient.
/// </summary>
[ByRefEvent]
public record struct TransferDnaEvent()
{
/// <summary>
/// The entity donating the DNA.
/// </summary>
public EntityUid Donor;
/// <summary>
/// The entity receiving the DNA.
/// </summary>
public EntityUid Recipient;
/// <summary>
/// Can the DNA be cleaned off?
/// </summary>
public bool CanDnaBeCleaned = true;
}

View File

@@ -8,6 +8,7 @@ namespace Content.Shared.Forensics
public readonly List<string> Fingerprints = new();
public readonly List<string> Fibers = new();
public readonly List<string> DNAs = new();
public readonly List<string> Residues = new();
public readonly string LastScannedName = string.Empty;
public readonly TimeSpan PrintCooldown = TimeSpan.Zero;
public readonly TimeSpan PrintReadyAt = TimeSpan.Zero;
@@ -16,6 +17,7 @@ namespace Content.Shared.Forensics
List<string> fingerprints,
List<string> fibers,
List<string> dnas,
List<string> residues,
string lastScannedName,
TimeSpan printCooldown,
TimeSpan printReadyAt)
@@ -23,6 +25,7 @@ namespace Content.Shared.Forensics
Fingerprints = fingerprints;
Fibers = fibers;
DNAs = dnas;
Residues = residues;
LastScannedName = lastScannedName;
PrintCooldown = printCooldown;
PrintReadyAt = printReadyAt;

View File

@@ -3,6 +3,7 @@ using System.Linq;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Forensics;
using Content.Shared.IdentityManagement;
using Content.Shared.Implants.Components;
using Content.Shared.Popups;
@@ -72,6 +73,9 @@ public abstract class SharedImplanterSystem : EntitySystem
else
ImplantMode(implanter, component);
var ev = new TransferDnaEvent { Donor = target, Recipient = implanter };
RaiseLocalEvent(target, ref ev);
Dirty(component);
}
@@ -140,6 +144,10 @@ public abstract class SharedImplanterSystem : EntitySystem
implantComp.ImplantedEntity = null;
implanterContainer.Insert(implant);
permanentFound = implantComp.Permanent;
var ev = new TransferDnaEvent { Donor = target, Recipient = implanter };
RaiseLocalEvent(target, ref ev);
//Break so only one implant is drawn
break;
}

View File

@@ -2,6 +2,7 @@ forensic-scanner-interface-title = Forensic scanner
forensic-scanner-interface-fingerprints = Fingerprints
forensic-scanner-interface-fibers = Fibers
forensic-scanner-interface-dnas = DNAs
forensic-scanner-interface-residues = Residues
forensic-scanner-interface-no-data = No scan data available
forensic-scanner-interface-print = Print
forensic-scanner-interface-clear = Clear

View File

@@ -0,0 +1,11 @@
forensic-residue = {LOC($adjective)} residue
forensic-residue-colored = {LOC($adjective)} {LOC($color)} residue
residue-unknown = unknown
residue-slippery = slippery
residue-green = green
residue-blue = blue
residue-red = red
residue-grey = grey
residue-brown = brown

View File

@@ -611,3 +611,6 @@
tags:
- DroneUsable
- Mop
- CleansForensics
- type: Fiber
fiberColor: fibers-white

View File

@@ -7,6 +7,7 @@
- type: Tag
tags:
- Soap
- CleansForensics
- type: Sprite
sprite: Objects/Specific/Janitorial/soap.rsi
layers:
@@ -68,6 +69,9 @@
- type: Food
solution: soap
- type: BadFood
- type: Residue
residueAdjective: residue-slippery
residueColor: residue-green
- type: entity
name: soap
@@ -90,6 +94,9 @@
reagents:
- ReagentId: SoapReagent
Quantity: 100
- type: Residue
residueAdjective: residue-slippery
residueColor: residue-grey
- type: entity
name: soap
@@ -105,6 +112,9 @@
fillBaseName: deluxe-
- type: Item
heldPrefix: deluxe
- type: Residue
residueAdjective: residue-slippery
residueColor: residue-brown
- type: entity
name: soap
@@ -127,6 +137,9 @@
flavors:
- clean
- punishment
- type: Residue
residueAdjective: residue-slippery
residueColor: residue-red
- type: entity
name: soaplet
@@ -189,6 +202,9 @@
flavors:
- clean
- meaty
- type: Residue
residueAdjective: residue-slippery
residueColor: residue-red
- type: entity
name: omega soap
@@ -214,3 +230,6 @@
reagents:
- ReagentId: SoapReagent
Quantity: 240
- type: Residue
residueAdjective: residue-slippery
residueColor: residue-blue

View File

@@ -1124,3 +1124,6 @@
- type: Tag
id: boots
- type: Tag
id: CleansForensics