import { describe, expect, it } from 'vitest'; import type { GameData, MachineDef, MachineKind, SimEvent } from '../contracts'; import { createAccentLookup, machineChassis } from './palette'; import { eventToast, jamText, machineFlavor } from './voice'; function def(over: Partial = {}): MachineDef { return { id: 'quantizer', name: 'QUANTIZER', codex: '', kind: 'crafter', footprint: { x: 2, y: 2 }, recipes: [], powerDraw: 0, asset: 'quantizer', ...over, }; } const named = (id: number) => (id === 5 ? 'MOSH REACTOR' : `UNIT ${id}`); describe('machineFlavor', () => { it('prefers the codex line from data once LANE-DATA fills it', () => { expect(machineFlavor(def({ flavor: 'The dial goes to CRIME.' }), 'quantizer')) .toBe('The dial goes to CRIME.'); }); it('falls back to the hand-transcribed line while data is empty', () => { expect(machineFlavor(def(), 'quantizer')).toContain('Rounds coefficients to zero'); }); it('gives an unknown machine generic copy rather than nothing', () => { expect(machineFlavor(def({ id: 'wormhole' }), 'wormhole')).toContain('Industrial equipment'); }); it('survives a missing def', () => { expect(machineFlavor(undefined, 'belt')).toContain('Moving things is the entire job'); }); }); describe('jamText', () => { it('distinguishes backed-up from starved', () => { expect(jamText('output full')).toBe('OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM.'); expect(jamText('starved')).toBe('INTAKE STARVED. NOTHING IS ARRIVING.'); }); it('passes an unknown reason through uppercased rather than swallowing it', () => { expect(jamText('belt on fire')).toBe('BELT ON FIRE'); }); }); describe('eventToast', () => { it('names the machine that jammed and says why', () => { const e: SimEvent = { kind: 'jammed', entity: 5, reason: 'starved', tick: 1 }; expect(eventToast(e, named)).toBe('MOSH REACTOR: INTAKE STARVED. NOTHING IS ARRIVING.'); }); it('has different copy for a brownout arriving and lifting', () => { const on = eventToast({ kind: 'brownout', on: true, tick: 1 }, named); const off = eventToast({ kind: 'brownout', on: false, tick: 2 }, named); expect(on).not.toBe(off); expect(on).toContain('STILL LIFE'); }); it('reports a thermal scram against the machine that scrammed', () => { const e: SimEvent = { kind: 'scram', entity: 5, on: true, tick: 9 }; expect(eventToast(e, named)).toContain('MOSH REACTOR'); expect(eventToast(e, named)).toContain('THERMAL SCRAM'); }); it('confirms a removal, because demolition should feel acknowledged', () => { const e: SimEvent = { kind: 'removed', entity: 1, def: 'belt', tick: 1 }; expect(eventToast(e, named)).toBe('UNIT RECLAIMED. THE FLOOR REMEMBERS.'); }); it('stays quiet for the high-frequency events', () => { for (const e of [ { kind: 'shipped', item: 'melt', count: 1, tick: 1 }, { kind: 'crafted', entity: 1, recipe: 'mosh', tick: 1 }, { kind: 'placed', entity: 1, def: 'belt', tick: 1 }, ] as SimEvent[]) { expect(eventToast(e, named)).toBeNull(); } }); }); describe('machineChassis', () => { it('uses the art-directed body colour from data', () => { expect(machineChassis(def({ color: '#4e5a62' }))).toBe('#4e5a62'); }); it('falls back to a neutral for a machine DATA has not art-directed', () => { expect(machineChassis(def({ color: undefined }))).toMatch(/^#/); }); }); describe('createAccentLookup', () => { // contracts v3: color is the chassis; the accent derives from what the machine makes. const data: GameData = { items: [ { id: 'melt', name: 'MELT', codex: '', tier: 2, color: '#ff7a3f' }, { id: 'hf-dust', name: 'HF DUST', codex: '', tier: 1, color: '#e8e0ff' }, ], machines: [], recipes: [ { id: 'mosh', machine: 'mosh-reactor', inputs: {}, outputs: { melt: 1 }, ticks: 90, bandwidth: 10 }, ], tech: [], commissions: [], }; const accentOf = createAccentLookup(data); it('derives the accent from the first thing the machine outputs', () => { expect(accentOf(def({ id: 'mosh-reactor', recipes: ['mosh'] }))).toBe('#ff7a3f'); }); it('falls back to the codex glow for machines that output nothing', () => { // Belts carry; they do not produce. Their accent has to come from the codex table. expect(accentOf(def({ id: 'belt', kind: 'belt', recipes: [] }))).toBe('#5a7a5a'); }); it('falls back by kind for an unknown machine with no recipes', () => { expect(accentOf(def({ id: 'brand-new-thing', kind: 'power', recipes: [] }))).toBe('#3fffe0'); }); it('ignores a recipe id that does not resolve rather than throwing', () => { expect(accentOf(def({ id: 'x', kind: 'crafter', recipes: ['ghost-recipe'] }))).toBe('#3fffe0'); }); it('always returns something for an unheard-of kind', () => { expect(accentOf(def({ id: 'x', kind: 'teleporter' as MachineKind, recipes: [] }))).toMatch(/^#/); }); });