glytch/fktry/src/contracts.ts
type-two 86d765f90b [orchestrator] round 1 review + contracts v2 + round 2 orders
Review: all 5 lanes pass — 70/70 tests, tsc clean, ownership audit clean,
integration verified live (sim -> events -> UI ticker + SCREEN corruption).

Contracts v2 (granted from lane requests): UIBus.pickTile (wired), BeltItem.id?,
MachineDef.{color,flavor,heatPerTick,bufferCap}?, CommissionDef.repeat?,
SimEvent 'scram'. main.ts: non-left pointerdown no longer places. happy-dom added.

Rulings: canon melt chain IS M1 (masterplan updated); rank-routing stays, splitters
next; anchor-slab sanitizing -> Correction buyback fiction; build-bar paging approved;
per-lane dev ports 8151-8155. Round 2 orders written in all lane docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:42:58 +10:00

169 lines
6.5 KiB
TypeScript

/**
* FKTRY shared contracts — ORCHESTRATOR-OWNED.
* Every lane imports from this file. NO lane edits it. If a contract blocks you,
* write the problem in your lane doc's NOTES and stub around it locally.
*/
export const TICKS_PER_SECOND = 30;
export const TILE = 1; // world units per grid tile
export type Dir = 0 | 1 | 2 | 3; // N E S W (clockwise)
export interface Vec2 { x: number; y: number }
// ---------------------------------------------------------------- data schemas
// These mirror data/*.json (LANE-DATA owns the JSON, orchestrator owns the shape).
export interface ItemDef {
id: string; // kebab-case, matches codex name, e.g. "chroma-slurry"
name: string; // display, e.g. "CHROMA SLURRY"
codex: string; // section ref into docs/FKTRY_LORE.md, e.g. "2:chroma-slurry"
tier: number; // 0 raw, 1 refined, 2 product, 3 endgame
color: string; // hex, used by renderer placeholders + UI chips
}
export interface RecipeDef {
id: string;
machine: string; // MachineDef.id that can run it
inputs: Record<string, number>; // itemId -> count
outputs: Record<string, number>;
ticks: number; // duration at speed 1
bandwidth: number; // power draw while crafting (negative = generates)
heat?: number; // heat per craft (software decoders etc.)
}
export type MachineKind =
| 'extractor' // pulls items from seam tiles, no inputs
| 'crafter' // recipe in -> out
| 'belt' // moves items
| 'splitter' // 1 in, N out round-robin
| 'power' // generates bandwidth
| 'buffer' // stores bandwidth smoothness (brownout protection)
| 'shipper'; // consumes items => 'shipped' events => THE SCREEN
export interface MachineDef {
id: string;
name: string;
codex: string;
kind: MachineKind;
footprint: Vec2; // tiles occupied (w,h) before rotation
recipes: string[]; // RecipeDef.ids this machine may run ([] for belts etc.)
powerDraw: number; // idle bandwidth draw
powerGen?: number;
beltSpeed?: number; // tiles/sec, kind==='belt'
asset: string; // codex asset key for renderer mesh registry, e.g. "quantizer"
color?: string; // v2: art-direction accent; renderer falls back to derived
flavor?: string; // v2: codex one-liner for the inspector (DATA's material)
heatPerTick?: number; // v2: heat while active (the software decoder's whole bit)
bufferCap?: number; // v2: bandwidth storage, kind==='buffer'
}
export interface TechDef {
id: string;
era: 'reel' | 'broadcast' | 'disc' | 'stream' | 'fortress';
cost: Record<string, number>; // science-pack itemId -> count
unlocks: string[]; // machine/recipe ids
}
export interface CommissionDef {
id: string;
flavor: string; // the fax text — deadpan absurd
wants: Record<string, number>;
rewardBandwidth: number;
repeat?: boolean; // v2: standing order — re-activates after completion
}
export interface GameData {
items: ItemDef[];
machines: MachineDef[];
recipes: RecipeDef[];
tech: TechDef[];
commissions: CommissionDef[];
}
// ---------------------------------------------------------------- live sim state
export interface EntityState {
id: number;
def: string; // MachineDef.id
pos: Vec2; // tile coords of origin corner
dir: Dir;
recipe: string | null;
progress: number; // 0..1 of current craft
inputBuf: Record<string, number>;
outputBuf: Record<string, number>;
jammed: string | null; // human-readable reason or null
heat: number; // 0..1, throttles >0.7, scrams at 1
}
export interface BeltItem {
item: string;
entity: number; // belt entity id
t: number; // 0..1 progress along the belt tile
id?: number; // v2: stable per-item id for exact render interpolation
// (optional this round; hardens to required in v3)
}
export interface SimSnapshot {
tick: number;
paused: boolean;
entities: ReadonlyArray<EntityState>;
beltItems: ReadonlyArray<BeltItem>;
bandwidth: { gen: number; draw: number; stored: number; brownout: boolean };
shippedTotal: Record<string, number>;
activeCommission: string | null;
commissionProgress: Record<string, number>;
}
export type Command =
| { kind: 'place'; def: string; pos: Vec2; dir: Dir }
| { kind: 'remove'; pos: Vec2 }
| { kind: 'rotate'; pos: Vec2 }
| { kind: 'setRecipe'; entity: number; recipe: string | null }
| { kind: 'setPaused'; paused: boolean };
export type SimEvent =
| { kind: 'shipped'; item: string; count: number; tick: number }
| { kind: 'crafted'; entity: number; recipe: string; tick: number }
| { kind: 'jammed'; entity: number; reason: string; tick: number }
| { kind: 'brownout'; on: boolean; tick: number }
| { kind: 'placed'; entity: number; def: string; tick: number }
| { kind: 'removed'; entity: number; def: string; tick: number }
| { kind: 'commissionDone'; commission: string; tick: number }
| { kind: 'scram'; entity: number; on: boolean; tick: number }; // v2: heat shutdown edge
// ---------------------------------------------------------------- module interfaces
export interface Sim {
init(data: GameData, seed: number): void;
enqueue(cmd: Command): void;
tick(): void; // advance exactly one tick
snapshot(): SimSnapshot; // read-only view; renderer/ui must not mutate
drainEvents(): SimEvent[]; // events since last drain (main loop fans out)
}
export interface Renderer {
init(container: HTMLElement, data: GameData): Promise<void>;
render(snap: SimSnapshot, alpha: number): void; // alpha: 0..1 interp between ticks
pickTile(clientX: number, clientY: number): Vec2 | null;
setGhost(def: string | null, pos: Vec2 | null, dir: Dir): void;
}
export interface UIBus {
dispatch(cmd: Command): void;
/** current build selection; renderer ghost + click-to-place read this */
selectedBuild(): { def: string; dir: Dir } | null;
/** v2: world picking, plumbed from the renderer — UI's click-to-inspect */
pickTile(clientX: number, clientY: number): Vec2 | null;
}
export interface UI {
init(root: HTMLElement, data: GameData, bus: UIBus): void;
update(snap: SimSnapshot, events: SimEvent[]): void;
}
export interface ScreenFX {
init(canvas: HTMLCanvasElement, data: GameData): void;
onEvents(events: SimEvent[]): void; // shipped items feed the corruption
frame(timeMs: number): void;
}