multirepo
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||
import { BehaviourDisplayInfo, BehaviourLink, BehaviourSocket, Resource, Resources, SerializedBehaviour } from "../types";
|
||||
|
||||
export abstract class Behaviour {
|
||||
public readonly id: string;
|
||||
public readonly kind: string;
|
||||
public readonly inputLinks: BehaviourLink[];
|
||||
|
||||
constructor(source: SerializedBehaviour) {
|
||||
this.id = source.id;
|
||||
this.kind = source.kind;
|
||||
this.inputLinks = source.inputLinks.pins;
|
||||
}
|
||||
|
||||
public getDisplayInfo(): BehaviourDisplayInfo {
|
||||
return {
|
||||
storages: {},
|
||||
sockets: [
|
||||
...this.inputs.map((input, index) => ({
|
||||
type: 'input',
|
||||
index,
|
||||
resource: input,
|
||||
} satisfies BehaviourSocket)),
|
||||
...this.outputs.map((output, index) => ({
|
||||
type: 'output',
|
||||
index,
|
||||
resource: output,
|
||||
} satisfies BehaviourSocket)),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public abstract get inputs(): Resource[];
|
||||
public abstract get outputs(): Resource[];
|
||||
|
||||
/** Withdraws up to `amount` of `resource` from this behaviour's storage, returning how much was actually taken. */
|
||||
public abstract take(resource: Resource, amount: DecimalSource): Decimal;
|
||||
|
||||
protected abstract give(resource: Resource, amount: DecimalSource): void;
|
||||
|
||||
/** Snapshot of everything currently held, keyed by resource. */
|
||||
public abstract getStorage(): Partial<Resources>;
|
||||
|
||||
abstract reset(): void;
|
||||
|
||||
/**
|
||||
* Advances this behaviour by `deltaTime`. Must pull whatever inputs it needs (via `resolve`)
|
||||
* before advancing its own cycle, so that a cycle only completes once enough input was actually
|
||||
* available — otherwise it stalls and tries again on a later tick.
|
||||
*
|
||||
* `fairShare(sourceId, resource)` caps how much of `resource` may be pulled from `sourceId` this
|
||||
* tick: when several behaviours are linked to the same source for the same resource, each is
|
||||
* capped to an equal share of what that source held right after its own tick, regardless of
|
||||
* pull order, instead of whoever asks first draining it.
|
||||
*/
|
||||
abstract tick(
|
||||
deltaTime: number,
|
||||
resolve: (id: string) => Behaviour | undefined,
|
||||
fairShare: (sourceId: string, resource: Resource) => Decimal,
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||
import { 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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './base';
|
||||
export * from './converter';
|
||||
export * from './storage';
|
||||
@@ -0,0 +1,83 @@
|
||||
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||
import { BehaviourDisplayInfo, Resource, Resources, SerializedBehaviour, SerializedStorage } from "../types";
|
||||
import { Behaviour } from "./base";
|
||||
|
||||
export class Storage extends Behaviour {
|
||||
public readonly capacity: Decimal | undefined;
|
||||
public readonly acceptsResources: Resource[];
|
||||
public readonly items: Partial<Resources>;
|
||||
|
||||
constructor(source: SerializedBehaviour & SerializedStorage) {
|
||||
if (source.kind !== 'storage')
|
||||
throw new Error('Invalid source');
|
||||
super(source);
|
||||
|
||||
this.capacity = source.capacity ? new Decimal(source.capacity) : undefined;
|
||||
this.acceptsResources = source.acceptsResources;
|
||||
this.items = {};
|
||||
for (const [res, amount] of Object.entries(source.items ?? {}))
|
||||
this.items[res as Resource] = new Decimal(amount);
|
||||
}
|
||||
|
||||
public get inputs(): Resource[] {
|
||||
return this.acceptsResources;
|
||||
}
|
||||
|
||||
public get outputs(): Resource[] {
|
||||
return this.acceptsResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* A storage has no rate of its own — it pulls as much as its linked source is willing to give
|
||||
* (up to whatever room its own capacity leaves), and later hands out as much as whoever calls
|
||||
* `take` asks for (up to whatever it actually has). The speed in either direction is entirely
|
||||
* up to the counterpart, not this behaviour.
|
||||
*/
|
||||
public tick(
|
||||
_deltaTime: number,
|
||||
resolve: (id: string) => Behaviour | undefined,
|
||||
fairShare: (sourceId: string, resource: Resource) => Decimal,
|
||||
): void {
|
||||
for (const resource of this.acceptsResources) {
|
||||
const link = this.inputLinks.find((l) => l.resource === resource);
|
||||
if (!link)
|
||||
continue;
|
||||
|
||||
const source = resolve(link.target);
|
||||
if (!source)
|
||||
continue;
|
||||
|
||||
const have = this.items[resource] ?? Decimal.dZero;
|
||||
const room = this.capacity ? this.capacity.sub(have).max(0) : Decimal.dInf;
|
||||
const request = room.min(fairShare(link.target, link.resource));
|
||||
if (request.gt(0))
|
||||
this.give(resource, source.take(link.resource, request));
|
||||
}
|
||||
}
|
||||
|
||||
public take(resource: Resource, amount: DecimalSource): Decimal {
|
||||
const available = this.items[resource] ?? Decimal.dZero;
|
||||
const taken = available.min(amount);
|
||||
this.items[resource] = available.sub(taken);
|
||||
return taken;
|
||||
}
|
||||
|
||||
protected give(resource: Resource, amount: DecimalSource): void {
|
||||
this.items[resource] = (this.items[resource] ?? Decimal.dZero).add(amount);
|
||||
}
|
||||
|
||||
public getStorage(): Partial<Resources> {
|
||||
return { ...this.items };
|
||||
}
|
||||
|
||||
public getDisplayInfo(): BehaviourDisplayInfo {
|
||||
const info = super.getDisplayInfo();
|
||||
info.storages['Хранилище'] = { ...this.items } as Resources;
|
||||
return info;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
for (const res of Object.keys(this.items))
|
||||
delete (this.items[res as Resource]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user