diff --git a/src/engine/functions/index.ts b/src/engine/functions/index.ts index bdfa8f1..306e865 100644 --- a/src/engine/functions/index.ts +++ b/src/engine/functions/index.ts @@ -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; public readonly outputRates: Partial; - /** 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); - } + 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 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. */ - 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 { + let affordableCycles = wantedCycles; 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); + 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)); } } diff --git a/src/types/index.ts b/src/types/index.ts index b38e9ff..14dac7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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(); + 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() {