[screen] boredom: the sky forgets, the ledger does not

No shipments for a sustained stretch and the corruption eases back toward clean.
The LEDGER is untouched -- history is not erased, only the display decays -- so
resuming production puts the targets straight back and the picture walks home in
about a second.

60s grace then 180s decay: a jam you notice, diagnose and fix costs nothing.
The player has to genuinely walk away before the sky starts cleaning itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 20:01:38 +10:00
parent 83e3ad2107
commit 5ad7252e20
2 changed files with 74 additions and 0 deletions

View File

@ -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);
});
});

View File

@ -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);
}