diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index e9432ee..4f1bccf 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -73,7 +73,7 @@ const NodeCard = forwardRef(function NodeCard({ node2d, s info.links.map((link) =>
→{link.target}/{link.resource}
) } -
{JSON.stringify(info, undefined, 4)}
+
{JSON.stringify(info.raw, undefined, 4)}
); }); diff --git a/src/engine/functions/index.ts b/src/engine/functions/index.ts index a2f4c05..bdfa8f1 100644 --- a/src/engine/functions/index.ts +++ b/src/engine/functions/index.ts @@ -35,6 +35,8 @@ export type BehaviourDisplayInfo = { storages: Record; sockets: BehaviourSocket[]; links: BehaviourLink[]; + + [x: string]: unknown; } export abstract class Behaviour { @@ -42,7 +44,6 @@ export abstract class Behaviour { public readonly kind: string; public readonly inputs: BehaviourInputs; public readonly outputs: BehaviourOutputs; - protected readonly storage: Partial = {}; constructor(source: SerializedBehaviour) { this.id = source.id; @@ -60,69 +61,9 @@ export abstract class Behaviour { } } - public reset() { - for (const res of Object.keys(this.storage)) - delete (this.storage[res as Resource]); - } - - abstract getDisplayInfo(): BehaviourDisplayInfo; - - /** Withdraws up to `amount` of `resource` from this behaviour's storage, returning how much was actually taken. */ - 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); - } - - abstract produce(deltaTime: number): void; - - abstract pull(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void; -} - -export class Converter extends Behaviour { - public readonly inputRates: Partial; - public readonly outputRates: Partial; - - constructor(source: SerializedNodeBehaviour & SerializedConverter) { - if (source.kind !== 'converter') - throw new Error('Invalid source'); - super(source); - - this.inputRates = {}; - for (const pin of source.inputs.pins) - this.inputRates[pin.resource] = new Decimal(pin.rate); - - this.outputRates = {}; - for (const pin of source.outputs.pins) - this.outputRates[pin.resource] = new Decimal(pin.rate); - } - - /** Fills this converter's own storage from its outputs' production rates. */ - public produce(deltaTime: number): void { - for (const output of this.outputs.pins) - this.give(output.resource, (this.outputRates[output.resource] ?? Decimal.dZero).mul(deltaTime)); - } - - /** Pulls resources into this converter's storage from whatever each input pin is linked to. */ - public pull(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void { - 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(deltaTime)); - this.give(input.resource, taken); - } - } - public getDisplayInfo(): BehaviourDisplayInfo { return { - storages: { 'Буфер': { ...this.storage } as Resources }, + storages: {}, links: this.inputs.pins.map((input) => input.link), sockets: [ ...this.inputs.pins.map((input, index) => ({ @@ -136,8 +77,114 @@ export class Converter extends Behaviour { resource: output.resource, } satisfies BehaviourSocket)), ], + }; + } + + /** 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; + + 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; +} + +export class Converter extends Behaviour { + protected readonly storage: Partial; + public cycleTime: Decimal; + 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') + 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 tick(deltaTime: number): void { + this.cycleTime = this.cycleTime.add(deltaTime); + const fullCycles = this.cycleTime.div(this.cycleLength).floor(); + this.cycleTime = this.cycleTime.sub(fullCycles.mul(this.cycleLength)); + + 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 { + 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); } } + + 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 { + return { ...this.storage }; + } + + public getDisplayInfo(): BehaviourDisplayInfo { + const info = super.getDisplayInfo(); + info.storages['Буфер'] = { ...this.storage } as Resources; + return { + ...info, + raw: { + cycleTime: this.cycleTime, + cycleLength: this.cycleLength, + inputRates: this.inputRates, + outputRates: this.outputRates, + }, + } as any; + } + + public reset() { + for (const res of Object.keys(this.storage)) + delete (this.storage[res as Resource]); + } } // export class Storage extends Behaviour { diff --git a/src/engine/saveLoad.ts b/src/engine/saveLoad.ts index b9fd064..da85763 100644 --- a/src/engine/saveLoad.ts +++ b/src/engine/saveLoad.ts @@ -9,6 +9,8 @@ export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviou return { id: converter.id, kind: 'converter', + cycleTime: converter.cycleTime, + storage: converter.getStorage(), inputs: { pins: converter.inputs.pins.map((pin) => ({ ...pin, @@ -21,6 +23,11 @@ export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviou rate: converter.outputRates[pin.resource] ?? 0, })), }, + rules: { + cycleLength: converter.cycleLength, + inputRates: converter.inputRates, + outputRates: converter.outputRates, + } }; } default: diff --git a/src/prefabs/game.ts b/src/prefabs/game.ts index 714706e..9e080a3 100644 --- a/src/prefabs/game.ts +++ b/src/prefabs/game.ts @@ -16,14 +16,16 @@ export const defaultGame: () => SerializedGame = () => ({ pins: [ { resource: 'wood', - rate: 1, }, { resource: 'stone', - rate: 0.1, }, ], }, + rules: { + cycleLength: 1, + outputRates: { wood: 1, stone: 0.1 }, + } }, }, { @@ -36,7 +38,6 @@ export const defaultGame: () => SerializedGame = () => ({ pins: [ { resource: 'wood', - rate: 0.5, link: { target: 'forest.1', resource: 'wood' }, }, ] @@ -45,10 +46,14 @@ export const defaultGame: () => SerializedGame = () => ({ pins: [ { resource: 'plank', - rate: 1, }, ], }, + rules: { + cycleLength: 1.8, + inputRates: { wood: 1 }, + outputRates: { plank: 2 }, + }, }, }, { @@ -61,7 +66,6 @@ export const defaultGame: () => SerializedGame = () => ({ pins: [ { resource: 'plank', - rate: 1, link: { target: 'sawmill.1', resource: 'plank' }, }, ], @@ -69,6 +73,10 @@ export const defaultGame: () => SerializedGame = () => ({ outputs: { pins: [], }, + rules: { + cycleLength: 1, + inputRates: { plank: 1 }, + }, }, }, ], diff --git a/src/types/index.ts b/src/types/index.ts index 51d0318..b38e9ff 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,14 +31,14 @@ export class Game { const behavioursById = new Map(this.nodes.map((node) => [node.behaviour.id, node.behaviour])); for (const node of this.nodes) - node.behaviour.produce(deltaTime); + node.behaviour.tick(deltaTime); for (const node of this.nodes) - node.behaviour.pull(deltaTime, (id) => behavioursById.get(id)); + node.behaviour.pull((id) => behavioursById.get(id)); } public reset() { this.time = Decimal.dZero; - for (const node of this.nodes) + for (const node of this.nodes) node.behaviour.reset(); } @@ -48,30 +48,35 @@ export class Game { } } -export type SerializedConverterInput = { +export type SerializedInput = { resource: Resource; - rate: DecimalSource; link: { target: SerializedNodeBehaviour['id'], resource: Resource, } } -export type SerializedConverterOutput = { +export type SerializedOutput = { resource: Resource; - rate: DecimalSource; } -export type SerializedConverterInputs = { - pins: SerializedConverterInput[]; +export type SerializedInputs = { + pins: SerializedInput[]; } -export type SerializedConverterOutputs = { - pins: SerializedConverterOutput[]; +export type SerializedOutputs = { + pins: SerializedOutput[]; } export type SerializedConverter = { kind: 'converter'; + cycleTime?: DecimalSource; + storage?: Partial>; + rules: { + cycleLength: DecimalSource; + inputRates?: Partial>; + outputRates?: Partial>; + } } export type SerializedStorage = { @@ -80,8 +85,8 @@ export type SerializedStorage = { export type SerializedNodeBehaviour = { id: string; - inputs: SerializedConverterInputs; - outputs: SerializedConverterOutputs; + inputs: SerializedInputs; + outputs: SerializedOutputs; } & (SerializedConverter | SerializedStorage); export type SerializedNode = {