1
Vessels and Substances
flan edited this page 2026-07-16 03:13:22 +00:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Vessels and Substances

Foul Play splits every throwable into two axes that never touch:

  • The vessel (CompVessel) is the container — how it is delivered.
  • The substance (SubstanceDef) is the fluid — what it does when it lands.

A "piss jar" is the intersection: a jar (vessel) of urine (substance). This page is about how the two combine at runtime, and the single most important fact about the whole framework: a vessel holds a weighted blend, and every consequence reads that blend, not the def.


A vessel is two numbers

RimWorld has no carryable containers of its own, so Foul Play ships a capability instead of a family. Any ThingDef becomes a throwable vessel just by carrying CompProperties_Vessel, which declares two things and lets everything else derive:

<li Class="FoulPlay.CompProperties_Vessel">
  <volumeMl>500</volumeMl>   <!-- capacity in millilitres -->
  <sealed_>true</sealed_>    <!-- corked/waxed, or open like a bucket -->
  <substance>PN_Urine</substance>  <!-- the default fill -->
</li>

Everything physical about the weapon is a function of those two numbers, derived once in VesselProfile so the derivation is consistent across our vessels and any modded one. The reference point is a 5 L carboy (RefVolumeMl = 5000); VolumeFraction = Clamp(volumeMl / 5000, 0.05, 4).

Derived property Formula Sealed → Open → Consumed by
Blast radius 4.5 × VolumeFraction^(1/3) ×1.0 ×1.35 (slops wider) the splash
Dose density constant 1.0 0.55 (lost some in transit) splash damage + infection dose
Filth count max(2, round(4 × √VolumeFraction)) — — how much mess it leaves
Spill per cell constant 0.0 0.06 (design model; see below)
Throw range factor Lerp(1.3, 0.6, InverseLerp(0.1, 1.6, VF)) ×1.0 ×0.65 (design model; see below)
Fill ticks round((volumeMl / mlPerDay) × 60000) — — how long a body takes to fill it

Blast is a cube-root of volume because a splash is a volume spreading over an area — doubling the litres does not double the radius. A sealed vessel bursts concentrated; an open one has already lost liquid down the carrier's leg and spreads the rest thin.

Honesty note. Blast radius, dose density and filth count are read live by the splash. Spill-per-cell and throw-range-factor are part of the same single-source derivation model but are not yet wired into a live per-cell spill or a range multiplier; the open vessel's "wears it on the way" character is currently expressed at throw time as a tripled self-splash chance (0.18 → 0.54). The model is the design contract; the splash is where it currently bites.

The four starter vessels, derived

Concrete numbers for the Piss Nuke family, all falling straight out of the two dials:

Vessel Vol Sealed Blast radius Dose density Filth Fill time (urine, 1750 ml/day)
Piss Jar 500 ml yes ~2.1 1.0 2 ~7 in-game hours
Piss Bottle 750 ml yes ~2.4 1.0 2 ~10 in-game hours
Piss Bucket 8000 ml no ~7.1 0.55 5 ~4.6 days
Piss Nuke 5000 ml yes ~4.5 1.0 4 ~2.9 days

The bucket holds sixteen times the jar but throws shorter, lands wider, and arrives thinner — the whole personality of an open container, none of it hand-authored.


A vessel holds a blend, not a substance

Here is the part that turns four items into a system. CompVessel does not store "the substance." It stores a weighted blend — a dictionary of how much of each substance is present, by volume in ml:

amounts = { FP_Urine: 250,  FP_Feces: 250 }   // a half-and-half cocktail in a 500 ml jar

A single substance is just a blend of one. A never-touched vessel reports null and behaves as a full vessel of its def's substance until you pour something in — at which point the amounts materialise and can be mutated.

The four measurements

Every downstream system asks the blend the same handful of questions:

Property Meaning
Capacity how much it holds full, in ml (= volumeMl)
TotalVolume how much liquid is in it right now (a never-touched vessel = full of its def)
Headroom Capacity − TotalVolume — room left to pour more in
FractionOf(sub) that substance's share of the blend, amount / total, in 0..1

IsFull is Headroom ≤ 0.001. Headroom is the gatekeeper of mixing: you cannot pour into a vessel with no headroom. (See Mixing and Reactions.)

The dominant substance

Substance (singular) returns the biggest share — the substance a mostly-piss cocktail "is." It is used for the label, for self-splash on a throw ("whatever they mostly slopped on themselves"), and anywhere a single answer is needed. Setting it replaces the whole blend with a full vessel of that one substance.


Everything scales by share

This is the payoff, and it is uniform across the entire mod. No consequence looks at the def's declared substance; each looks at the runtime blend and scales by each substance's fraction. A drop of hooch in a jar of piss barely warms you; a jar that is mostly hooch is a firebomb. Ratios decide the dose.

System How the share is applied
Throwing / splash the projectile carries the whole blend; each constituent lands its own splash at potency × fraction. A thrown cocktail applies every substance in its ratios.
Drinking PostIngested walks the blend and applies each substance's drink effect (hediff, thought, nutrition, addiction) scaled by its fraction. A memory below a 0.2 share is not even worth remembering.
Addiction addictionRarity × fraction × dispositionMultiplier — a splash of luciferium in a jar rolls at a fraction of a full jar's odds.
Reactions water douses fire volume-for-volume; a splash of water in a carboy of chemfuel barely dents it, a cup of hooch in a bucket of water is simply gone.
Label a real cocktail relabels itself with the shares: piss jar (urine 60%, hooch 40%).

Contents-aware labels

Because contents are runtime, a "piss jar" the def shipped may actually hold a vile cocktail. CompVessel tells the truth in the label:

  • A cocktail shows the shares, dominant first: piss jar (feces 50%, urine 50%).
  • A single substance that is not the def's own: piss jar (hooch).
  • A fermenting or spoiled vessel appends its state: piss nuke, fermenting 42% / piss nuke, spoiled. Only a full-strength (≥95% potency), undiluted, pure vessel gets to wear its plain name — which is the mechanical root of "a carboy is only a piss nuke when it is 100% ripe pure urine." (See The Piss Nuke.)

For modders

Add a throwable container to any mod in three lines of XML: give a ThingDef a CompProperties_Vessel with volumeMl, sealed_, and a default substance. You inherit correct fill time, blast, spill, splash shape, mixing, fermentation-readiness, drink behaviour and contents labels for free — no per-container code. Attach a new SubstanceDef (see Substance Catalogue) and any vessel can now hold it. The two axes are orthogonal by construction: N vessels × M substances is N×M weapons from N+M defs.

The contents blend is exposed as a flat string via SerializeContents() / DeserializeContents() so a content-agnostic layer (like Contraband's concealment) can store a cocktail blind and hand it back whole.


Foul Play is part of the Institution suite — one install with a checkbox per layer (Core, Contraband, Policing, Corrections, Gangs, Ward) about what an institution does to the people inside it. It bridges to the Contraband layer when Institution is installed.