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;
|
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);
|
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 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Fills this converter's own storage from its outputs' production rates. */
|
let affordableCycles = wantedCycles;
|
||||||
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);
|
|
||||||
if (!source)
|
|
||||||
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));
|
if (rate.eq(0))
|
||||||
this.give(input.resource, taken);
|
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;
|
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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user