diff --git a/README.md b/README.md index 74ca6fd..f560f82 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,17 @@ it (char 99.9% → 54.5%, evenness 0.029 → 0.226). The judge notices both. And MITEY is its own skill: spread it on, then take almost all of it back off. He wants to see the toast *through* it. +Peanut butter comes smooth or crunchy, and both are *natural* — the jar +separates while it sits. The oil rises; the paste underneath dries out. An +unstirred first dip is a slick (flows at almost no pressure, goes on thin and +shiny), the dips after it are grout (fights like cold butter). Swirl the knife +in the jar — about two and a half circles — to mix it back, and yes, stirring +costs time, and time costs toast warmth. The judge reads the mass-weighted mean +*and spread* of the consistency that actually landed: `oil 0.50 ±0.00 — just +right` versus `oil 0.54 ±0.23 — slick here, grout there`, because a slick strip +and a grout strip average to "correct" and nobody eats the average. Crunchy +additionally has chunks, and the chunks are judged like rind (below). + Marmalade adds a second axis: the rind. Bits drop off the blade **per unit of stroke distance**, so a dabbing knife makes a pile and a moving knife lays a trail — and because the shed rate scales with what's left on the blade, one dip diff --git a/src/dev.ts b/src/dev.ts index 0bdc4af..df49bdc 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -87,6 +87,21 @@ export function installDevHarness(app: App, game: Game): void { return harness.stats(); }, + /** Swirl the knife in the jar — remixes a separated spread. */ + swirl(circles = 3) { + const hit = (kitchen as unknown as { potHit: THREE.Mesh | null }).potHit; + const wp = new THREE.Vector3(3.15, 0.56, 0.45); + hit?.getWorldPosition(wp); + const steps = Math.round(circles * 48); + for (let i = 0; i <= steps; i++) { + const a = (i / 48) * Math.PI * 2; + point(wp.x + Math.cos(a) * 0.2, wp.y, wp.z + Math.sin(a) * 0.2, true); + app.step(1 / 60); + } + app.input.down = false; + app.step(1 / 60); + }, + /** Park the cursor off the toast so a screenshot isn't full of knife. */ park() { point(2.5, kitchen.currentSlice.topY, 1.3, false); diff --git a/src/game/game.ts b/src/game/game.ts index 4a3cbc6..9e514f0 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -6,6 +6,7 @@ import { DrawerView } from '../scenes/drawer'; import { TOOLS, type ToolId } from '../sim/cutlery'; import { audio } from '../core/audio'; import { coolStep } from '../sim/toasting'; +import { SPREADS } from '../sim/spreads'; import { judge, type Grade } from './judging'; import { DAYS, proceduralOrder, nameBrowning, type Order } from './orders'; import { el, Panel } from '../ui/hud'; @@ -158,6 +159,9 @@ export class Game { this.ticketEl, `butter is ${o.butterSoftness > 0.6 ? 'soft' : o.butterSoftness > 0.3 ? 'firm' : 'fridge-hard'} today`, ); + if (SPREADS[o.spread].separates) { + el('div', 'ticket-hint', this.ticketEl, 'the jar has been sitting — stir it before you dip'); + } } /** Called when the player hands the plate over. */ diff --git a/src/game/judging.ts b/src/game/judging.ts index c7997d0..c2a11f6 100644 --- a/src/game/judging.ts +++ b/src/game/judging.ts @@ -90,6 +90,7 @@ export function judge(slice: Slice, order: Order, usedTool: ToolId, seconds: num detail: d.mean < 0.002 ? 'intact' : `torn over ${Math.round(slice.damage.fraction(slice.mask, (v) => v > 0.02) * 100)}%`, }, ...(def.particles ? [rindCriterion(slice, def.particles.name)] : []), + ...(def.separates ? [consistencyCriterion(slice)] : []), { key: 'amount', label: 'The Right Amount', @@ -184,6 +185,40 @@ function rindCriterion(slice: Slice, name: string): Criterion { }; } +/** + * How the spread that actually landed compares to a properly stirred jar. + * 0.5 is right; the slick off the top of an unstirred jar pushes it up, the + * dried paste underneath pulls it down. The fix was always the same: stir. + */ +function consistencyCriterion(slice: Slice): Criterion { + const oil = slice.oilMean; + const spread = slice.oilStdev; + const off = Math.abs(oil - 0.5); + // Two ways to fail: wrong on average, or right on average and wrong in every + // individual bite. The stdev term is what stops "one oily strip, one dry + // strip" from scoring as though the jar had been stirred. + const score = clamp01(1 - off / 0.32 - spread / 0.24); + const word = + spread > 0.15 + ? 'slick here, grout there' + : oil > 0.78 + ? 'a slick' + : oil > 0.62 + ? 'oily' + : oil >= 0.38 + ? 'just right' + : oil >= 0.22 + ? 'dry' + : 'grout'; + return { + key: 'consistency', + label: 'Consistency', + score, + weight: 1.1, + detail: `oil ${oil.toFixed(2)} ±${spread.toFixed(2)} — ${word}`, + }; +} + function cap(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } diff --git a/src/game/lines.ts b/src/game/lines.ts index 239db78..d68d8d9 100644 --- a/src/game/lines.ts +++ b/src/game/lines.ts @@ -108,16 +108,30 @@ const BANK: Bank = { }, rind: { bad: [ - 'The rind is all in one postcode.', - 'You have made a toast with a rind district.', - 'Every piece of rind, huddled together for warmth. Spread them OUT.', - 'One bite of this is all rind. The others are an apology.', - 'There is no rind on this. Did you eat it? You ate it.', + 'All the {bits}, in one postcode.', + 'You have made a toast with a {bits} district.', + 'Every piece of {bits}, huddled together for warmth. Spread them OUT.', + 'One bite of this is all {bits}. The others are an apology.', + 'There is no {bits} on this. Did you eat it? You ate it.', ], good: [ - 'The rind is evenly strewn. That takes patience, and you had it.', - 'Every bite gets its piece of rind. That is the entire craft.', - 'Rind distribution: textbook. I did not expect that today.', + 'Evenly strewn {bits}. That takes patience, and you had it.', + 'Every bite gets its {bits}. That is the entire craft.', + 'The {bits}: textbook distribution. I did not expect that today.', + ], + }, + consistency: { + bad: [ + 'It is weeping oil. I can hear it.', + 'You did not stir the jar. The jar knows. I know.', + 'This has the texture of grout. Grout with ambitions.', + 'Half slick, half plaster. Pick a toast.', + 'The oil went on first and the cement went on after. Stir. The. Jar.', + ], + good: [ + 'Properly stirred. You can taste the difference, and I intend to.', + 'The consistency is exactly right. Someone respected the jar.', + 'Smooth, even, no slick. The jar was stirred by an adult.', ], }, tool: { @@ -168,6 +182,7 @@ function fill(s: string, order: Order, v: Verdict, tool: string): string { .replace('{got}', v.criteria.find((c) => c.key === 'amount')?.detail.split(' — ')[0] ?? 'that') // Mid-sentence, so lowercase — except MITEY, which is shouted on principle. .replace('{spread}', spread.id === 'mitey' ? 'MITEY' : spread.name.toLowerCase()) + .replaceAll('{bits}', spread.particles?.name ?? 'rind') .replace('{tool}', tool.toLowerCase()); } diff --git a/src/game/orders.ts b/src/game/orders.ts index 830cc4d..e49eb5c 100644 --- a/src/game/orders.ts +++ b/src/game/orders.ts @@ -112,7 +112,7 @@ export const DAYS: Order[] = [ { day: 6, bread: 'sourdough', - spread: 'peanut', + spread: 'crunchy', amount: 'normal', browning: 0.72, browningName: 'dark', @@ -120,7 +120,7 @@ export const DAYS: Order[] = [ tool: 'steak_knife', butterSoftness: 0.25, who: 'a man in a hurry', - text: "Dark. Sourdough. Don't care if it catches a bit. Peanut butter, even.", + text: "Dark. Sourdough. Crunchy peanut butter — the PROPER kind. Don't care if it catches a bit.", }, { day: 7, @@ -153,7 +153,7 @@ export const DAYS: Order[] = [ /** Days 9+ — keep going, with everything cranked. */ export function proceduralOrder(day: number, rand: () => number): Order { const breads: BreadId[] = ['white', 'multigrain', 'raisin', 'sourdough']; - const spreads: SpreadId[] = ['butter', 'peanut', 'mitey', 'marmalade']; + const spreads: SpreadId[] = ['butter', 'peanut', 'crunchy', 'mitey', 'marmalade']; const amounts: AmountClass[] = ['thin', 'normal', 'thick']; const tools: ToolId[] = [ 'butter_knife', diff --git a/src/scenes/kitchen.ts b/src/scenes/kitchen.ts index 4c2a145..bc97f14 100644 --- a/src/scenes/kitchen.ts +++ b/src/scenes/kitchen.ts @@ -60,6 +60,11 @@ export class KitchenView implements View { private toastSeconds = 0; knife: Knife = newKnife(); + /** How badly the jar has separated right now. 0 = mixed, 1 = oil on top. */ + strat = 0; + private dipsSinceStir = 0; + private stirPrev: number | null = null; + private wasDipping = false; private knifeRig: KnifeRig; private pot: THREE.Group | null = null; private potHit: THREE.Mesh | null = null; @@ -82,6 +87,8 @@ export class KitchenView implements View { private gaugeScrape!: HTMLElement; private modeEl!: HTMLElement; private loadFill!: HTMLElement; + private jarRow!: HTMLElement; + private jarFill!: HTMLElement; private breadSel!: HTMLSelectElement; private spreadSel!: HTMLSelectElement; private toolSel!: HTMLSelectElement; @@ -184,6 +191,12 @@ export class KitchenView implements View { this.butterSoftness > 0.66 ? 'soft' : this.butterSoftness > 0.33 ? 'ok' : 'hard'; }); + this.jarRow = el('div', 'row', p); + el('label', 'lbl', this.jarRow, 'JAR'); + const jarBar = el('div', 'load', this.jarRow); + jarBar.style.flex = '1'; + this.jarFill = el('div', 'load-fill', jarBar); + // The pressure gauge — the mechanic made legible. The fill is the pressure // your wrist angle is producing; the gold mark is what this spread needs in // order to flow; the red mark is where the knife stops spreading and starts @@ -206,6 +219,10 @@ export class KitchenView implements View { setSpread(id: SpreadId): void { this.spreadId = id; const def = SPREADS[id]; + this.strat = def.separates ?? 0; + this.dipsSinceStir = 0; + this.stirPrev = null; + this.wasDipping = false; this.slice?.setSpread(def); this.knifeRig.setSpread(def); if (this.pot) this.root.remove(this.pot); @@ -263,6 +280,19 @@ export class KitchenView implements View { this.onServed?.(); } + /** + * What the next scoop out of the jar is like. The first dip off a split jar + * takes the oil slick; the ones after it dig into what the oil left behind. + * Stirred (strat 0), every dip is 0.5 — exactly right. + */ + private jarOil(): number { + if (this.strat <= 0.001) return 0.5; + const swing = 0.5 * this.strat; + // dip 0: +swing (the slick) · dip 1: about even · dip 2+: increasingly dry + const t = 1 - this.dipsSinceStir * 0.55; + return 0.5 + swing * Math.max(-1, Math.min(1, t)); + } + /** Whatever you managed to pull out of the drawer is what you're spreading with. */ setTool(id: ToolId): void { this.toolId = id; @@ -395,10 +425,46 @@ export class KitchenView implements View { if (hits.length) { overPot = true; this.knifeRig.update(hits[0].point, this.knife.angle * 0.3, this.knife.load, hits[0].point.y); - if (inp.down) dip(this.knife, def); + if (inp.down) { + // Swirling: angular travel of the knife around the jar's centre. Only + // sweep counts — holding still in the jar stirs nothing. + if (def.separates && this.strat > 0.001) { + const ang = Math.atan2(hits[0].point.z - POT_Z, hits[0].point.x - POT_X); + if (this.stirPrev !== null) { + let d = ang - this.stirPrev; + if (d > Math.PI) d -= Math.PI * 2; + if (d < -Math.PI) d += Math.PI * 2; + // ~2.5 full circles takes the jar from fully split to mixed. + const sweep = Math.abs(d); + if (sweep > 0.002) { + this.strat = Math.max(0, this.strat - sweep / (5 * Math.PI)); + this.dipsSinceStir = 0; + if (sweep > 0.02) audio.scrape(0.25, sweep * 6); + } + } + this.stirPrev = ang; + } + // A dip is an event, not a state: the oil you get depends on how many + // scoops have already come off the top since the last stir. + if (!this.wasDipping) { + dip(this.knife, def, this.jarOil()); + this.dipsSinceStir++; + this.wasDipping = true; + } else { + // Holding the blade in the jar keeps it topped up at the same blend. + dip(this.knife, def, this.knife.oil); + } + } else { + this.stirPrev = null; + this.wasDipping = false; + } this.knife.hasPrev = false; } } + if (!overPot) { + this.stirPrev = null; + this.wasDipping = false; + } if (!overPot) { this.plane.constant = -this.slice.topY; @@ -428,6 +494,7 @@ export class KitchenView implements View { } } + this.slice.uniforms.uOil.value = def.separates ? this.slice.oilMean : 0.5; this.updateGauge(def); } @@ -447,6 +514,13 @@ export class KitchenView implements View { ? '#6cbf6c' : '#8a7f70'; + this.jarRow.style.display = def.separates ? '' : 'none'; + if (def.separates) { + const mixed = 1 - this.strat; + this.jarFill.style.width = `${Math.round(mixed * 100)}%`; + this.jarFill.style.background = mixed > 0.75 ? '#6cbf6c' : mixed > 0.4 ? '#e8a53a' : '#c0392b'; + } + const fx = this.lastFx; let msg = scraping ? 'scraping' : 'spreading'; if (fx) { @@ -456,7 +530,10 @@ export class KitchenView implements View { else if (fx.mode === 'scrape') msg = 'taking it back off'; else if (fx.mode === 'spread') msg = 'spreading nicely'; } else if (this.knife.load <= 0.01) { - msg = 'the knife is empty — go and dip'; + msg = + def.separates && this.strat > 0.45 + ? 'the oil has risen — swirl the knife in the jar first' + : 'the knife is empty — go and dip'; } this.modeEl.textContent = msg; this.modeEl.style.color = diff --git a/src/sim/slice.ts b/src/sim/slice.ts index 0c8f42a..809faa2 100644 --- a/src/sim/slice.ts +++ b/src/sim/slice.ts @@ -56,6 +56,15 @@ export class Slice { private rindMesh: THREE.InstancedMesh | null = null; private rindDirty = false; + /** + * Consistency bookkeeping for spreads that separate: every deposit remembers + * the oil level it landed at, and the judge reads the mass-weighted mean. + * 0.5 is right; 1 is a slick; 0 is grout. + */ + private spreadMassSum = 0; + private oilMassSum = 0; + private oilSqSum = 0; + private tex: THREE.DataTexture; private texData: Uint8Array; private dirty = true; @@ -107,6 +116,36 @@ export class Slice { return this.mesh.position.y + this.halfThickness; } + recordDeposit(mass: number, oil: number): void { + this.spreadMassSum += mass; + this.oilMassSum += mass * oil; + this.oilSqSum += mass * oil * oil; + } + + /** Scraping takes spread off at whatever the current blend is. */ + recordRemoval(mass: number): void { + const mean = this.oilMean; + this.spreadMassSum = Math.max(0, this.spreadMassSum - mass); + this.oilMassSum = Math.max(0, this.oilMassSum - mass * mean); + this.oilSqSum = Math.max(0, this.oilSqSum - mass * mean * mean); + } + + /** Mass-weighted consistency of everything on the toast. 0.5 until proven otherwise. */ + get oilMean(): number { + return this.spreadMassSum < 1 ? 0.5 : this.oilMassSum / this.spreadMassSum; + } + + /** + * Mass-weighted spread of consistencies. This is what catches the lazy play: + * a slick strip and a grout strip AVERAGE to "just right" — but nobody wants + * to eat the average, they eat one bite at a time. + */ + get oilStdev(): number { + if (this.spreadMassSum < 1) return 0; + const mean = this.oilMean; + return Math.sqrt(Math.max(0, this.oilSqSum / this.spreadMassSum - mean * mean)); + } + /** Drop a bit of rind at (u,v). It rides the slice from then on. */ addRind(u: number, v: number): void { if (this.rind.length >= RIND_MAX) return; @@ -412,6 +451,7 @@ uniform vec3 uSpreadColor; uniform float uSpreadGloss; uniform float uSpreadOpaqueAt; // spread thickness at which it fully hides the toast uniform float uSpreadBump; +uniform float uOil; uniform vec3 uLightDir; uniform vec3 uLightColor; uniform vec3 uAmbientSky; @@ -542,6 +582,11 @@ void main() { // a little grain in the film so thin spread doesn't look like flat paint float filmN = fbm(vUv * 70.0 + 13.0); vec3 spreadCol = uSpreadColor * (0.85 + filmN * 0.3); + // Consistency tint: an oily slick is darker and saturated; dried-out paste + // goes pale and grey. uOil sits at 0.5 for anything that doesn't separate. + float oilT = (uOil - 0.5) * 2.0; + spreadCol *= 1.0 - max(oilT, 0.0) * 0.28; + spreadCol = mix(spreadCol, vec3(dot(spreadCol, vec3(0.333))) * 1.45, max(-oilT, 0.0) * 0.45); albedo = mix(albedo, spreadCol, cover); // Thickness relief. Central differences in *UV* units, not texels — a smooth @@ -573,6 +618,8 @@ void main() { // so pair a broad sheen with a fresnel rim — that's the cue that says "wet". float gloss = uSpreadGloss * smoothstep(0.008, 0.09, spread) * topFace; gloss *= 1.0 + uWarmth * 0.6; + // Oil is what shines. Dry paste barely does. + gloss *= 0.55 + uOil * 0.9; // The lobe has to be TIGHT. The slice is flat and both the light and the camera // are above it, so dot(N,H) is ~0.98 across the whole surface — any broad lobe // blankets it in white and lifts near-black MITEY to tan. Tight, and the only @@ -629,6 +676,7 @@ function buildMaterial(bread: Bread, tex: THREE.DataTexture): THREE.ShaderMateri uSpreadGloss: { value: 0.5 }, uSpreadOpaqueAt: { value: 1e9 }, uSpreadBump: { value: 0.1 }, + uOil: { value: 0.5 }, uLightDir: { value: new THREE.Vector3(0.45, 0.85, 0.35).normalize() }, uLightColor: { value: new THREE.Color(1.0, 0.95, 0.86) }, uAmbientSky: { value: new THREE.Color(0.26, 0.27, 0.31) }, diff --git a/src/sim/spreading.ts b/src/sim/spreading.ts index e4a36c7..dce7d22 100644 --- a/src/sim/spreading.ts +++ b/src/sim/spreading.ts @@ -45,6 +45,11 @@ export interface Knife { rindLoad: number; /** Fractional shed accumulator — pieces drop one at a time. */ rindFrac: number; + /** + * Consistency of what's on the blade: 0.5 is properly mixed, 1 is the oil + * slick off the top of an unstirred jar, 0 is the dried-out paste under it. + */ + oil: number; u: number; v: number; hasPrev: boolean; @@ -69,11 +74,13 @@ export interface StrokeFx { export function newKnife(): Knife { // Starts inside the window where warm-toast butter actually flows: the very // first stroke of the game shouldn't be a mysterious failure. - return { angle: 0.45, load: 0, rindLoad: 0, rindFrac: 0, u: 0.5, v: 0.5, hasPrev: false }; + return { angle: 0.45, load: 0, rindLoad: 0, rindFrac: 0, oil: 0.5, u: 0.5, v: 0.5, hasPrev: false }; } -export function dip(knife: Knife, def: SpreadDef): void { +/** `oil` is what the jar handed over — 0.5 unless the jar has stratified. */ +export function dip(knife: Knife, def: SpreadDef, oil = 0.5): void { knife.load = def.pickup; + knife.oil = oil; if (def.particles) { knife.rindLoad = def.particles.perDip; knife.rindFrac = 0; @@ -169,7 +176,11 @@ function contact( if (knife.angle < SCRAPE_ANGLE) { if (knife.load <= 0.0005) return; - const yieldP = effectiveYield(def, softness, slice.warmth); + // Consistency pushes back through the same physics as everything else: + // an oily load flows at almost no pressure, a dried-out one fights like + // cold butter. Only spreads that separate care. + const oilK = def.separates ? 1.25 - knife.oil * 0.5 : 1; + const yieldP = effectiveYield(def, softness, slice.warmth) * oilK; const flowing = pressure >= yieldP; const deficit = flowing ? 0 : Math.min(1, (yieldP - pressure) / Math.max(yieldP, 0.001)); fx.deficit = Math.max(fx.deficit, deficit); @@ -178,18 +189,25 @@ function contact( // Below the yield pressure the spread won't flow, so the blade grabs the // crumb and takes it with it. Some still smears on, badly. const transfer = flowing ? 1 : 0.25; - const rate = def.transferRate * tool.transferScale * dt * transfer; + // Runny goes on thin, dry goes on in slabs. + const oilRate = def.separates ? 1.15 - knife.oil * 0.4 : 1; + const rate = def.transferRate * tool.transferScale * dt * transfer * oilRate; let massLeft = knife.load * slice.mask.n * slice.mask.n; + let laid = 0; slice.spread.brush(u, v, radius, (i, w) => { if (mask[i] < 0.5 || massLeft <= 0) return; const blot = 1 - tool.blotch * blotchAt(i); const add = Math.min(rate * w * blot, massLeft); spread[i] += add; massLeft -= add; + laid += add; fx.deposited += add; }); knife.load = Math.max(0, massLeft / (slice.mask.n * slice.mask.n)); + // What lands remembers the consistency it landed at — the judge reads the + // mass-weighted mean at the end. + slice.recordDeposit(laid, knife.oil); // Suspended solids drop off the blade as it works — per unit of stroke // DISTANCE, not per second. That's the whole feel: a moving knife lays an @@ -259,6 +277,7 @@ function contact( // What comes off the toast mostly stays on the blade. knife.load += (removedSpread * 0.6) / (slice.mask.n * slice.mask.n); + slice.recordRemoval(removedSpread); fx.scrapedSpread += removedSpread; fx.scrapedChar += removedChar; fx.gouged += gouged; diff --git a/src/sim/spreads.ts b/src/sim/spreads.ts index 293f156..84c98f6 100644 --- a/src/sim/spreads.ts +++ b/src/sim/spreads.ts @@ -8,7 +8,7 @@ * better technique, it's warmer toast: don't dawdle in the drawer. */ -export type SpreadId = 'butter' | 'peanut' | 'mitey' | 'marmalade'; +export type SpreadId = 'butter' | 'peanut' | 'crunchy' | 'mitey' | 'marmalade'; export type AmountClass = 'thin' | 'normal' | 'thick'; /** @@ -45,6 +45,13 @@ export interface SpreadDef { bump: number; /** Suspended solids, if any. */ particles?: ParticleDef; + /** + * Natural nut butters stratify: how badly the jar has separated when the + * order starts (0..1). The oil rises, the paste below dries out — the first + * unstirred dip is a slick and the ones after it are grout. Swirl the knife + * in the jar to mix it back; the fix costs time, and time costs toast warmth. + */ + separates?: number; /** Pressure needed to make it flow, before any warming. */ baseYield: number; @@ -84,12 +91,13 @@ export const SPREADS: Record = { }, peanut: { id: 'peanut', - name: 'Peanut Butter', - blurb: 'Goes on thick, drags like wet sand, never evens itself out.', + name: 'Smooth Peanut Butter', + blurb: 'Goes on thick, drags like wet sand — and the jar has been sitting.', color: [0.64, 0.41, 0.18], gloss: 0.3, opaqueAt: 0.16, bump: 0.28, + separates: 0.75, baseYield: 0.38, tempSoftening: 0.3, viscosity: 0.96, @@ -99,6 +107,32 @@ export const SPREADS: Record = { scrapeEase: 0.75, amounts: { thin: [0.18, 0.32], normal: [0.38, 0.68], thick: [0.75, 1.1] }, }, + crunchy: { + id: 'crunchy', + name: 'Crunchy Peanut Butter', + blurb: 'Everything smooth is, plus chunks with opinions about where they sit.', + color: [0.6, 0.38, 0.16], + gloss: 0.28, + opaqueAt: 0.17, + bump: 0.32, + // Chunks sink; the jar splits worse than the smooth stuff. + separates: 0.9, + particles: { + name: 'peanut chunks', + color: [0.78, 0.6, 0.34], + size: 0.042, + perDip: 8, + shedRate: 0.85, + }, + baseYield: 0.42, + tempSoftening: 0.3, + viscosity: 0.97, + pickup: 0.95, + transferRate: 4.3, + drag: 1.1, + scrapeEase: 0.7, + amounts: { thin: [0.18, 0.32], normal: [0.38, 0.68], thick: [0.75, 1.1] }, + }, mitey: { id: 'mitey', name: 'MITEY',