diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx index 3257aeb..d3594bb 100644 --- a/src/components/GameCanvas.tsx +++ b/src/components/GameCanvas.tsx @@ -92,7 +92,7 @@ export const GameCanvas = function ({
{ graphNodes.map((graphNode) => - graphNode.entity.getDisplayInfo().links.map((link, idx) => { + graphNode.entity.inputLinks.map((link, idx) => { const target = graphNodes.find((gn) => gn.entity.id === link.target); return target && ( ({ - resource: output.resource, - })), - } - this.inputs = { - pins: source.inputs.pins.map((input) => ({ - resource: input.resource, - link: input.link, - })), - } + this.inputLinks = source.inputLinks.pins; } public getDisplayInfo(): BehaviourDisplayInfo { return { storages: {}, - links: this.inputs.pins.map((input) => input.link), sockets: [ - ...this.inputs.pins.map((input, index) => ({ + ...this.inputs.map((input, index) => ({ type: 'input', index, - resource: input.resource, + resource: input, } satisfies BehaviourSocket)), - ...this.outputs.pins.map((output, index) => ({ + ...this.outputs.map((output, index) => ({ type: 'output', index, - resource: output.resource, + resource: output, } satisfies BehaviourSocket)), ], }; } + public abstract get inputs(): Resource[]; + public abstract get outputs(): Resource[]; + /** 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; diff --git a/src/engine/behaviours/converter.ts b/src/engine/behaviours/converter.ts index 40d06f8..541b135 100644 --- a/src/engine/behaviours/converter.ts +++ b/src/engine/behaviours/converter.ts @@ -30,6 +30,14 @@ export class Converter extends Behaviour { this.outputRates[res as Resource] = new Decimal(rate); } + public get inputs(): Resource[] { + return Object.keys(this.inputRates) as Resource[]; + } + + public get outputs(): Resource[] { + return Object.keys(this.outputRates) as Resource[]; + } + /** * A converter grabs a full batch of each input the moment enough is available — reserving * ahead for however many cycles (including the one currently underway) the elapsed time could @@ -44,21 +52,23 @@ export class Converter extends Behaviour { const reserveCycles = idealCycleTime.div(this.cycleLength).ceil(); let newCycleTime = idealCycleTime; - for (const input of this.inputs.pins) { - const rate = this.inputRates[input.resource] ?? Decimal.dZero; + for (const [res, rate] of Object.entries(this.inputRates) as [Resource, Decimal][]) { if (rate.eq(0)) continue; const target = rate.mul(reserveCycles); - const have = this.storage[input.resource] ?? Decimal.dZero; + const have = this.storage[res] ?? Decimal.dZero; const shortfall = target.sub(have).max(0); if (shortfall.gt(0)) { - const source = resolve(input.link.target); - if (source) - this.give(input.resource, source.take(input.link.resource, shortfall)); + const link = this.inputLinks.find((l) => l.resource === res); + if (link) { + const source = resolve(link.target); + if (source) + this.give(res, source.take(link.resource, shortfall)); + } } - const haveNow = this.storage[input.resource] ?? Decimal.dZero; + const haveNow = this.storage[res] ?? Decimal.dZero; newCycleTime = newCycleTime.min(haveNow.mul(this.cycleLength).div(rate)); } @@ -66,10 +76,10 @@ export class Converter extends Behaviour { const affordableCycles = this.cycleTime.div(this.cycleLength).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)); + for (const [res, rate] of Object.entries(this.inputRates) as [Resource, Decimal][]) + this.take(res, rate.mul(affordableCycles)); + for (const [res, rate] of Object.entries(this.outputRates) as [Resource, Decimal][]) + this.give(res, rate.mul(affordableCycles)); this.cycleTime = this.cycleTime.sub(affordableCycles.mul(this.cycleLength)); } } diff --git a/src/engine/behaviours/storage.ts b/src/engine/behaviours/storage.ts new file mode 100644 index 0000000..31dd4b8 --- /dev/null +++ b/src/engine/behaviours/storage.ts @@ -0,0 +1,78 @@ +import Decimal, { DecimalSource } from "break_eternity.js"; +import { BehaviourDisplayInfo, Resource, Resources, SerializedBehaviour, SerializedStorage } from "../../types"; +import { Behaviour } from "./base"; + +export class Storage extends Behaviour { + public readonly capacity: Decimal | undefined; + public readonly acceptsResources: Resource[]; + public readonly items: Partial; + + constructor(source: SerializedBehaviour & SerializedStorage) { + if (source.kind !== 'storage') + throw new Error('Invalid source'); + super(source); + + this.capacity = source.capacity ? new Decimal(source.capacity) : undefined; + this.acceptsResources = source.acceptsResources; + this.items = {}; + for (const [res, amount] of Object.entries(source.items ?? {})) + this.items[res as Resource] = new Decimal(amount); + } + + public get inputs(): Resource[] { + return this.acceptsResources; + } + + public get outputs(): Resource[] { + return this.acceptsResources; + } + + /** + * A storage has no rate of its own — it pulls as much as its linked source is willing to give + * (up to whatever room its own capacity leaves), and later hands out as much as whoever calls + * `take` asks for (up to whatever it actually has). The speed in either direction is entirely + * up to the counterpart, not this behaviour. + */ + public tick(_deltaTime: number, resolve: (id: string) => Behaviour | undefined): void { + for (const resource of this.acceptsResources) { + const link = this.inputLinks.find((l) => l.resource === resource); + if (!link) + continue; + + const source = resolve(link.target); + if (!source) + continue; + + const have = this.items[resource] ?? Decimal.dZero; + const room = this.capacity ? this.capacity.sub(have).max(0) : Decimal.dInf; + if (room.gt(0)) + this.give(resource, source.take(link.resource, room)); + } + } + + public take(resource: Resource, amount: DecimalSource): Decimal { + const available = this.items[resource] ?? Decimal.dZero; + const taken = available.min(amount); + this.items[resource] = available.sub(taken); + return taken; + } + + protected give(resource: Resource, amount: DecimalSource): void { + this.items[resource] = (this.items[resource] ?? Decimal.dZero).add(amount); + } + + public getStorage(): Partial { + return { ...this.items }; + } + + public getDisplayInfo(): BehaviourDisplayInfo { + const info = super.getDisplayInfo(); + info.storages['Хранилище'] = { ...this.items } as Resources; + return info; + } + + public reset() { + for (const res of Object.keys(this.items)) + delete (this.items[res as Resource]); + } +} diff --git a/src/engine/saveLoad.ts b/src/engine/saveLoad.ts index 4e8350e..268f0c1 100644 --- a/src/engine/saveLoad.ts +++ b/src/engine/saveLoad.ts @@ -1,7 +1,8 @@ import { defaultGame } from "../prefabs/game"; -import { SerializedBehaviour, SerializedGame } from "../types"; +import { SerializedBehaviour, SerializedConverter, SerializedGame, SerializedStorage } from "../types"; import { Behaviour } from "./behaviours/base"; import { Converter } from "./behaviours/converter"; +import { Storage } from "./behaviours/storage"; import { Game } from "./game"; export const saveParams = { @@ -9,33 +10,34 @@ export const saveParams = { } export function serializeBehaviour(behaviour: Behaviour): SerializedBehaviour { + const base = { + id: behaviour.id, + kind: behaviour.kind, + inputLinks: { pins: behaviour.inputLinks }, + }; + switch (behaviour.kind) { case 'converter': { const converter = behaviour as Converter; return { - id: converter.id, - kind: 'converter', + ...base, cycleTime: converter.cycleTime, storage: converter.getStorage(), - inputs: { - pins: converter.inputs.pins.map((pin) => ({ - ...pin, - rate: converter.inputRates[pin.resource] ?? 0, - })), - }, - outputs: { - pins: converter.outputs.pins.map((pin) => ({ - ...pin, - rate: converter.outputRates[pin.resource] ?? 0, - })), - }, rules: { cycleLength: converter.cycleLength, inputRates: converter.inputRates, outputRates: converter.outputRates, } - }; + } as (SerializedBehaviour & SerializedConverter);; } + case 'storage': + const storage = behaviour as Storage; + return { + ...base, + capacity: storage.capacity, + acceptsResources: storage.acceptsResources, + items: storage.getStorage(), + } as (SerializedBehaviour & SerializedStorage); default: throw new Error('Unknown behaviour'); } @@ -45,6 +47,8 @@ export function deserializeBehaviour(value: SerializedBehaviour): Behaviour { switch (value.kind) { case 'converter': return new Converter(value); + case 'storage': + return new Storage(value); default: throw new Error('Unknown behaviour'); } diff --git a/src/prefabs/game.ts b/src/prefabs/game.ts index 7980b6c..3497179 100644 --- a/src/prefabs/game.ts +++ b/src/prefabs/game.ts @@ -9,19 +9,7 @@ export const defaultGame: () => SerializedGame = () => ({ behaviour: { id: 'forest.1', kind: 'converter', - inputs: { - pins: [], - }, - outputs: { - pins: [ - { - resource: 'tree', - }, - { - resource: 'stone', - }, - ], - }, + inputLinks: { pins: [] }, rules: { cycleLength: 1, outputRates: { tree: 1, stone: 0.1 }, @@ -34,21 +22,14 @@ export const defaultGame: () => SerializedGame = () => ({ behaviour: { id: 'sawmill.1', kind: 'converter', - inputs: { + inputLinks: { pins: [ { resource: 'tree', - link: { target: 'forest.1', resource: 'tree' }, + target: 'forest.1', }, ] }, - outputs: { - pins: [ - { - resource: 'wood', - }, - ], - }, rules: { cycleLength: 1.8, inputRates: { tree: 1 }, @@ -57,26 +38,25 @@ export const defaultGame: () => SerializedGame = () => ({ }, }, { - id: 'burner', - name: 'Магазин досок', + id: 'shop', + name: 'Магазин', behaviour: { - id: 'burner.1', - kind: 'converter', - inputs: { + id: 'shop.1', + kind: 'storage', + inputLinks: { pins: [ { resource: 'wood', - link: { target: 'sawmill.1', resource: 'wood' }, + target: 'sawmill.1', }, - ], - }, - outputs: { - pins: [], - }, - rules: { - cycleLength: 1, - inputRates: { wood: 1 }, + { + resource: 'stone', + target: 'forest.1', + } + ] }, + acceptsResources: ['wood', 'stone'], + items: {}, }, }, ], diff --git a/src/types/behaviour.ts b/src/types/behaviour.ts index fc7fe79..67e881b 100644 --- a/src/types/behaviour.ts +++ b/src/types/behaviour.ts @@ -15,25 +15,9 @@ export type BehaviourLink = { resource: Resource; } -export type BehaviourInput = BehaviourResourcePin & { - link: BehaviourLink; -} - -export type BehaviourInputs = { - pins: BehaviourInput[]; -} - -export type BehaviourOutput = BehaviourResourcePin & { -} - -export type BehaviourOutputs = { - pins: BehaviourOutput[]; -} - export type BehaviourDisplayInfo = { storages: Record; sockets: BehaviourSocket[]; - links: BehaviourLink[]; [x: string]: unknown; } diff --git a/src/types/serialization.ts b/src/types/serialization.ts index 2ff79bf..59983da 100644 --- a/src/types/serialization.ts +++ b/src/types/serialization.ts @@ -2,24 +2,13 @@ import { Unit } from './unit'; import { DecimalSource } from "break_eternity.js"; import { Resource } from "./resource"; -export type SerializedInput = { - resource: Resource; - link: { - target: SerializedBehaviour['id'], - resource: Resource, - } +export type SerializedInputLink = { + target: SerializedBehaviour['id'], + resource: Resource, } -export type SerializedOutput = { - resource: Resource; -} - -export type SerializedInputs = { - pins: SerializedInput[]; -} - -export type SerializedOutputs = { - pins: SerializedOutput[]; +export type SerializedInputLinks = { + pins: SerializedInputLink[]; } export type SerializedConverter = { @@ -35,12 +24,14 @@ export type SerializedConverter = { export type SerializedStorage = { kind: 'storage', + capacity?: DecimalSource; + acceptsResources: Resource[]; + items: Partial>; } export type SerializedBehaviour = { id: string; - inputs: SerializedInputs; - outputs: SerializedOutputs; + inputLinks: SerializedInputLinks; } & (SerializedConverter | SerializedStorage); export type SerializedNode = { diff --git a/src/ui/graph.ts b/src/ui/graph.ts index 3d48c71..dd14280 100644 --- a/src/ui/graph.ts +++ b/src/ui/graph.ts @@ -30,7 +30,7 @@ export async function build2dNodes(units: Unit[], area: { width: number, height: height: NODE_HEIGHT, })), edges: units.flatMap((node) => - node.behaviour.getDisplayInfo().links.map((link, idx) => ({ + node.behaviour.inputLinks.map((link, idx) => ({ id: `${node.behaviour.id}->${link.target}.${idx}`, sources: [node.behaviour.id], targets: [link.target], diff --git a/src/utils/behaviourSort.ts b/src/utils/behaviourSort.ts index faa61a2..18b486b 100644 --- a/src/utils/behaviourSort.ts +++ b/src/utils/behaviourSort.ts @@ -10,8 +10,8 @@ export function orderByDependency(behaviours: 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); + for (const inputLink of behaviour.inputLinks) { + const dependency = byId.get(inputLink.target); if (dependency) visit(dependency); }