diff --git a/fktry/src/screen/boredom.test.ts b/fktry/src/screen/boredom.test.ts new file mode 100644 index 0000000..5832f53 --- /dev/null +++ b/fktry/src/screen/boredom.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { BOREDOM_GRACE_SECONDS, BOREDOM_HORIZON_SECONDS, freshness } from './boredom'; + +describe('boredom decays the display, slowly', () => { + it('shows everything you earned while production is running', () => { + expect(freshness(0)).toBe(1); + expect(freshness(5)).toBe(1); + }); + + it('does not punish a normal jam', () => { + // The whole grace window plus a beat: a stall long enough to notice, diagnose and fix + // must leave THE SCREEN exactly as the player left it. + expect(freshness(BOREDOM_GRACE_SECONDS)).toBe(1); + expect(freshness(BOREDOM_GRACE_SECONDS - 1)).toBe(1); + }); + + it('takes minutes, not seconds, to forget', () => { + // A minute of silence has barely dented it; the sky is patient. + expect(freshness(BOREDOM_GRACE_SECONDS + 30)).toBeGreaterThan(0.8); + // Fully clean only after the player has genuinely walked away. + expect(BOREDOM_GRACE_SECONDS + BOREDOM_HORIZON_SECONDS).toBeGreaterThanOrEqual(180); + }); + + it('reaches clean at the horizon and stays there', () => { + expect(freshness(BOREDOM_GRACE_SECONDS + BOREDOM_HORIZON_SECONDS)).toBe(0); + expect(freshness(BOREDOM_GRACE_SECONDS + BOREDOM_HORIZON_SECONDS + 9999)).toBe(0); + }); + + it('decays monotonically', () => { + let prev = 2; + for (let s = 0; s <= BOREDOM_GRACE_SECONDS + BOREDOM_HORIZON_SECONDS + 60; s += 5) { + const f = freshness(s); + expect(f).toBeLessThanOrEqual(prev); + expect(f).toBeGreaterThanOrEqual(0); + prev = f; + } + }); + + it('sits at half way through the decay', () => { + expect(freshness(BOREDOM_GRACE_SECONDS + BOREDOM_HORIZON_SECONDS / 2)).toBeCloseTo(0.5, 5); + }); + + it('treats a nonsense idle time as fresh rather than wiping the sky', () => { + expect(freshness(NaN)).toBe(1); + expect(freshness(-10)).toBe(1); + }); +}); diff --git a/fktry/src/screen/boredom.ts b/fktry/src/screen/boredom.ts new file mode 100644 index 0000000..9720560 --- /dev/null +++ b/fktry/src/screen/boredom.ts @@ -0,0 +1,27 @@ +/** + * Boredom — the factory's work evaporating off THE SCREEN. + * + * If nothing ships for a long stretch, the sky slowly forgets. The corruption LEDGER is + * untouched: history is not erased, only the display decays. So when production resumes the + * targets are instantly back where they were and the picture snaps home in about a second. + * + * The horizon is deliberately generous. A jam, a rebuild, a rethink of the bus — none of + * those are supposed to read as punishment. The player should have to genuinely walk away + * before the sky starts cleaning itself. + */ + +/** No shipments for this long before the sky begins to forget at all. */ +export const BOREDOM_GRACE_SECONDS = 60; +/** ...and this long again to fade from full corruption to clean. */ +export const BOREDOM_HORIZON_SECONDS = 180; + +const clamp01 = (x: number) => Math.min(1, Math.max(0, x)); + +/** + * How much of the ledger's corruption is still on display, 0..1. + * 1 = everything you earned is showing; 0 = the sky has forgotten (the tallies have not). + */ +export function freshness(idleSeconds: number): number { + if (!(idleSeconds > BOREDOM_GRACE_SECONDS)) return 1; // NaN-safe: unknown idle reads as fresh + return 1 - clamp01((idleSeconds - BOREDOM_GRACE_SECONDS) / BOREDOM_HORIZON_SECONDS); +}