converter work on demand
This commit is contained in:
@@ -85,14 +85,14 @@ export abstract class Behaviour {
|
||||
|
||||
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 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 {
|
||||
@@ -101,8 +101,6 @@ export class Converter extends Behaviour {
|
||||
public readonly cycleLength: Decimal;
|
||||
public readonly inputRates: 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) {
|
||||
if (source.kind !== 'converter')
|
||||
@@ -125,30 +123,44 @@ export class Converter extends Behaviour {
|
||||
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);
|
||||
const fullCycles = this.cycleTime.div(this.cycleLength).floor();
|
||||
this.cycleTime = this.cycleTime.sub(fullCycles.mul(this.cycleLength));
|
||||
const wantedCycles = this.cycleTime.div(this.cycleLength).floor();
|
||||
|
||||
this.pendingCycles = fullCycles;
|
||||
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 {
|
||||
if (wantedCycles.gt(0)) {
|
||||
for (const input of this.inputs.pins) {
|
||||
const source = resolve(input.link.target);
|
||||
if (!source)
|
||||
continue;
|
||||
const rate = this.inputRates[input.resource] ?? Decimal.dZero;
|
||||
const taken = source.take(input.link.resource, rate.mul(this.pendingCycles));
|
||||
this.give(input.resource, taken);
|
||||
const have = this.storage[input.resource] ?? Decimal.dZero;
|
||||
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
@@ -12,6 +12,30 @@ export type Node = {
|
||||
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 {
|
||||
public time: Decimal;
|
||||
public readonly nodes: Node[];
|
||||
@@ -29,11 +53,12 @@ export class Game {
|
||||
this.time = this.time.add(deltaTime);
|
||||
|
||||
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)
|
||||
node.behaviour.tick(deltaTime);
|
||||
for (const node of this.nodes)
|
||||
node.behaviour.pull((id) => behavioursById.get(id));
|
||||
// Producers must tick (and so fill their storage) before whatever pulls from them,
|
||||
// so resources can flow all the way down a chain within a single tick.
|
||||
for (const behaviour of orderByDependency(this.nodes.map((node) => node.behaviour)))
|
||||
behaviour.tick(deltaTime, resolve);
|
||||
}
|
||||
|
||||
public reset() {
|
||||
|
||||
Reference in New Issue
Block a user