import Decimal, { type DecimalSource } from "break_eternity.js"; import type { BehaviourDisplayInfo, Resource, Resources, SerializedBehaviour, SerializedConverter } from "../types"; import { Behaviour } from "./base"; export class Converter extends Behaviour { protected readonly storage: Partial; public cycleTime: Decimal; public readonly cycleLength: Decimal; public readonly inputRates: Partial; public readonly outputRates: Partial; constructor(source: SerializedBehaviour & SerializedConverter) { if (source.kind !== 'converter') throw new Error('Invalid source'); super(source); this.cycleTime = new Decimal(source.cycleTime); this.cycleLength = new Decimal(source.rules.cycleLength); this.storage = {}; for (const [res, amount] of Object.entries(source.storage ?? {})) this.storage[res as Resource] = new Decimal(amount); this.inputRates = {}; for (const [res, rate] of Object.entries(source.rules.inputRates ?? {})) this.inputRates[res as Resource] = new Decimal(rate); this.outputRates = {}; for (const [res, rate] of Object.entries(source.rules.outputRates ?? {})) this.outputRates[res as Resource] = new Decimal(rate); } public get inputs(): Resource[] { return Object.keys(this.inputRates) as Resource[]; } public get outputs(): Resource[] { return Object.keys(this.outputRates) as Resource[]; } /** * A converter grabs a full batch of each input the moment enough is available — reserving * ahead for however many cycles (including the one currently underway) the elapsed time could * reach, rather than trickling resources in proportionally as time passes. If the source can't * supply the full batch, `cycleTime` only advances as far as what was actually secured * justifies: a converter can't be "cooking" a batch it doesn't have the ingredients for, so it * stalls proportionally to whatever is actually missing rather than free-running on elapsed * time alone. */ public tick( deltaTime: number, resolve: (id: string) => Behaviour | undefined, fairShare: (sourceId: string, resource: Resource) => Decimal, ): void { const idealCycleTime = this.cycleTime.add(deltaTime); const reserveCycles = idealCycleTime.div(this.cycleLength).ceil(); let newCycleTime = idealCycleTime; for (const [res, rate] of Object.entries(this.inputRates) as [Resource, Decimal][]) { if (rate.eq(0)) continue; const target = rate.mul(reserveCycles); const have = this.storage[res] ?? Decimal.dZero; const shortfall = target.sub(have).max(0); if (shortfall.gt(0)) { const link = this.inputLinks.find((l) => l.resource === res); if (link) { const source = resolve(link.target); if (source) { const request = shortfall.min(fairShare(link.target, link.resource)); if (request.gt(0)) this.give(res, source.take(link.resource, request)); } } } const haveNow = this.storage[res] ?? Decimal.dZero; newCycleTime = newCycleTime.min(haveNow.mul(this.cycleLength).div(rate)); } this.cycleTime = newCycleTime; const affordableCycles = this.cycleTime.div(this.cycleLength).floor(); if (affordableCycles.gt(0)) { for (const [res, rate] of Object.entries(this.inputRates) as [Resource, Decimal][]) this.take(res, rate.mul(affordableCycles)); for (const [res, rate] of Object.entries(this.outputRates) as [Resource, Decimal][]) this.give(res, rate.mul(affordableCycles)); this.cycleTime = this.cycleTime.sub(affordableCycles.mul(this.cycleLength)); } } public take(resource: Resource, amount: DecimalSource): Decimal { const available = this.storage[resource] ?? Decimal.dZero; const taken = available.min(amount); this.storage[resource] = available.sub(taken); return taken; } protected give(resource: Resource, amount: DecimalSource): void { this.storage[resource] = (this.storage[resource] ?? Decimal.dZero).add(amount); } public getStorage(): Partial { return { ...this.storage }; } public getDisplayInfo(): BehaviourDisplayInfo { const info = super.getDisplayInfo(); info.storages['Буфер'] = { ...this.storage } as Resources; return { ...info, raw: { inputRates: this.inputRates, outputRates: this.outputRates, }, } as any; } public reset() { for (const res of Object.keys(this.storage)) delete (this.storage[res as Resource]); } }