converter work on demand

This commit is contained in:
azykov@mail.ru
2026-09-03 10:24:51 +03:00
parent b14b60858c
commit 4b4ab88918
2 changed files with 68 additions and 31 deletions
+37 -25
View File
@@ -85,14 +85,14 @@ export abstract class Behaviour {
protected abstract give(resource: Resource, amount: DecimalSource): void; protected abstract give(resource: Resource, amount: DecimalSource): void;
abstract produce(times: DecimalSource): void;
/** Pulls into this behaviour's own storage the same number of cycles this tick's `tick()` call just completed. */
abstract pull(resolve: (id: string) => Behaviour | undefined): void;
abstract reset(): void; abstract reset(): void;
abstract tick(deltaTime: number): 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.
*/
abstract tick(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void;
} }
export class Converter extends Behaviour { export class Converter extends Behaviour {
@@ -101,8 +101,6 @@ export class Converter extends Behaviour {
public readonly cycleLength: Decimal; public readonly cycleLength: Decimal;
public readonly inputRates: Partial<Resources>; public readonly inputRates: Partial<Resources>;
public readonly outputRates: Partial<Resources>; public readonly outputRates: Partial<Resources>;
/** Number of full cycles completed by the most recent `tick()` call, consumed by `pull()`. */
private pendingCycles: Decimal = Decimal.dZero;
constructor(source: SerializedNodeBehaviour & SerializedConverter) { constructor(source: SerializedNodeBehaviour & SerializedConverter) {
if (source.kind !== 'converter') if (source.kind !== 'converter')
@@ -125,30 +123,44 @@ export class Converter extends Behaviour {
this.outputRates[res as Resource] = new Decimal(rate); this.outputRates[res as Resource] = new Decimal(rate);
} }
public tick(deltaTime: number): void { /**
* Advances time, pulls as much input as is available toward however many cycles that time
* would allow, then completes only as many full cycles as the input actually on hand supports.
* Any leftover cycle time is kept for later — a starved converter simply stalls until its
* inputs catch up, and then bursts through the backlog once they do.
*/
public tick(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void {
this.cycleTime = this.cycleTime.add(deltaTime); this.cycleTime = this.cycleTime.add(deltaTime);
const fullCycles = this.cycleTime.div(this.cycleLength).floor(); const wantedCycles = this.cycleTime.div(this.cycleLength).floor();
this.cycleTime = this.cycleTime.sub(fullCycles.mul(this.cycleLength));
this.pendingCycles = fullCycles; if (wantedCycles.gt(0)) {
this.produce(fullCycles);
}
/** Fills this converter's own storage from its outputs' production rates. */
public produce(times: DecimalSource): void {
for (const output of this.outputs.pins)
this.give(output.resource, (this.outputRates[output.resource] ?? Decimal.dZero).mul(times));
}
/** Pulls resources into this converter's storage from whatever each input pin is linked to, scaled by the cycles this tick's `tick()` just completed. */
public pull(resolve: (id: string) => Behaviour | undefined): void {
for (const input of this.inputs.pins) { for (const input of this.inputs.pins) {
const source = resolve(input.link.target); const source = resolve(input.link.target);
if (!source) if (!source)
continue; continue;
const rate = this.inputRates[input.resource] ?? Decimal.dZero; const rate = this.inputRates[input.resource] ?? Decimal.dZero;
const taken = source.take(input.link.resource, rate.mul(this.pendingCycles)); const have = this.storage[input.resource] ?? Decimal.dZero;
this.give(input.resource, taken); const stillNeeded = rate.mul(wantedCycles).sub(have).max(0);
if (stillNeeded.gt(0))
this.give(input.resource, source.take(input.link.resource, stillNeeded));
}
}
let affordableCycles = wantedCycles;
for (const input of this.inputs.pins) {
const rate = this.inputRates[input.resource] ?? Decimal.dZero;
if (rate.eq(0))
continue;
const have = this.storage[input.resource] ?? Decimal.dZero;
affordableCycles = affordableCycles.min(have.div(rate).floor());
}
if (affordableCycles.gt(0)) {
for (const input of this.inputs.pins)
this.take(input.resource, (this.inputRates[input.resource] ?? Decimal.dZero).mul(affordableCycles));
for (const output of this.outputs.pins)
this.give(output.resource, (this.outputRates[output.resource] ?? Decimal.dZero).mul(affordableCycles));
this.cycleTime = this.cycleTime.sub(affordableCycles.mul(this.cycleLength));
} }
} }
+29 -4
View File
@@ -12,6 +12,30 @@ export type Node = {
behaviour: Behaviour; behaviour: Behaviour;
} }
/** Orders behaviours so that whatever a behaviour's inputs link to comes before it (a dependency cycle just falls back to encounter order). */
function orderByDependency(behaviours: Behaviour[]): Behaviour[] {
const byId = new Map(behaviours.map((behaviour) => [behaviour.id, behaviour]));
const visited = new Set<string>();
const order: Behaviour[] = [];
function visit(behaviour: Behaviour) {
if (visited.has(behaviour.id))
return;
visited.add(behaviour.id);
for (const input of behaviour.inputs.pins) {
const dependency = byId.get(input.link.target);
if (dependency)
visit(dependency);
}
order.push(behaviour);
}
for (const behaviour of behaviours)
visit(behaviour);
return order;
}
export class Game { export class Game {
public time: Decimal; public time: Decimal;
public readonly nodes: Node[]; public readonly nodes: Node[];
@@ -29,11 +53,12 @@ export class Game {
this.time = this.time.add(deltaTime); this.time = this.time.add(deltaTime);
const behavioursById = new Map(this.nodes.map((node) => [node.behaviour.id, node.behaviour])); const behavioursById = new Map(this.nodes.map((node) => [node.behaviour.id, node.behaviour]));
const resolve = (id: string) => behavioursById.get(id);
for (const node of this.nodes) // Producers must tick (and so fill their storage) before whatever pulls from them,
node.behaviour.tick(deltaTime); // so resources can flow all the way down a chain within a single tick.
for (const node of this.nodes) for (const behaviour of orderByDependency(this.nodes.map((node) => node.behaviour)))
node.behaviour.pull((id) => behavioursById.get(id)); behaviour.tick(deltaTime, resolve);
} }
public reset() { public reset() {