126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
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<Resources>;
|
|
public cycleTime: Decimal;
|
|
public readonly cycleLength: Decimal;
|
|
public readonly inputRates: Partial<Resources>;
|
|
public readonly outputRates: Partial<Resources>;
|
|
|
|
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<Resources> {
|
|
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]);
|
|
}
|
|
}
|