converter now has cycleLength. storage save load fix

This commit is contained in:
azykov@mail.ru
2026-09-01 09:52:52 +03:00
parent b9c0c0ee28
commit b14b60858c
5 changed files with 148 additions and 81 deletions
+1 -1
View File
@@ -73,7 +73,7 @@ const NodeCard = forwardRef<HTMLDivElement, Props>(function NodeCard({ node2d, s
info.links.map((link) => <div key={`${link.target}/${link.resource}`}>→{link.target}/{link.resource}</div>) info.links.map((link) => <div key={`${link.target}/${link.resource}`}>→{link.target}/{link.resource}</div>)
} }
</div> </div>
<div className="node__debug" style={{fontSize: '50%', whiteSpace: 'pre'}}>{JSON.stringify(info, undefined, 4)}</div> <div className="node__debug" style={{fontSize: '8px', whiteSpace: 'pre'}}>{JSON.stringify(info.raw, undefined, 4)}</div>
</div> </div>
); );
}); });
+109 -62
View File
@@ -35,6 +35,8 @@ export type BehaviourDisplayInfo = {
storages: Record<string, Resources>; storages: Record<string, Resources>;
sockets: BehaviourSocket[]; sockets: BehaviourSocket[];
links: BehaviourLink[]; links: BehaviourLink[];
[x: string]: unknown;
} }
export abstract class Behaviour { export abstract class Behaviour {
@@ -42,7 +44,6 @@ export abstract class Behaviour {
public readonly kind: string; public readonly kind: string;
public readonly inputs: BehaviourInputs; public readonly inputs: BehaviourInputs;
public readonly outputs: BehaviourOutputs; public readonly outputs: BehaviourOutputs;
protected readonly storage: Partial<Resources> = {};
constructor(source: SerializedBehaviour) { constructor(source: SerializedBehaviour) {
this.id = source.id; 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<Resources>;
public readonly outputRates: Partial<Resources>;
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 { public getDisplayInfo(): BehaviourDisplayInfo {
return { return {
storages: { 'Буфер': { ...this.storage } as Resources }, storages: {},
links: this.inputs.pins.map((input) => input.link), links: this.inputs.pins.map((input) => input.link),
sockets: [ sockets: [
...this.inputs.pins.map((input, index) => ({ ...this.inputs.pins.map((input, index) => ({
@@ -136,8 +77,114 @@ export class Converter extends Behaviour {
resource: output.resource, resource: output.resource,
} satisfies BehaviourSocket)), } 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<Resources>;
public cycleTime: Decimal;
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')
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<Resources> {
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 { // export class Storage extends Behaviour {
+7
View File
@@ -9,6 +9,8 @@ export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviou
return { return {
id: converter.id, id: converter.id,
kind: 'converter', kind: 'converter',
cycleTime: converter.cycleTime,
storage: converter.getStorage(),
inputs: { inputs: {
pins: converter.inputs.pins.map((pin) => ({ pins: converter.inputs.pins.map((pin) => ({
...pin, ...pin,
@@ -21,6 +23,11 @@ export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviou
rate: converter.outputRates[pin.resource] ?? 0, rate: converter.outputRates[pin.resource] ?? 0,
})), })),
}, },
rules: {
cycleLength: converter.cycleLength,
inputRates: converter.inputRates,
outputRates: converter.outputRates,
}
}; };
} }
default: default:
+13 -5
View File
@@ -16,14 +16,16 @@ export const defaultGame: () => SerializedGame = () => ({
pins: [ pins: [
{ {
resource: 'wood', resource: 'wood',
rate: 1,
}, },
{ {
resource: 'stone', resource: 'stone',
rate: 0.1,
}, },
], ],
}, },
rules: {
cycleLength: 1,
outputRates: { wood: 1, stone: 0.1 },
}
}, },
}, },
{ {
@@ -36,7 +38,6 @@ export const defaultGame: () => SerializedGame = () => ({
pins: [ pins: [
{ {
resource: 'wood', resource: 'wood',
rate: 0.5,
link: { target: 'forest.1', resource: 'wood' }, link: { target: 'forest.1', resource: 'wood' },
}, },
] ]
@@ -45,10 +46,14 @@ export const defaultGame: () => SerializedGame = () => ({
pins: [ pins: [
{ {
resource: 'plank', resource: 'plank',
rate: 1,
}, },
], ],
}, },
rules: {
cycleLength: 1.8,
inputRates: { wood: 1 },
outputRates: { plank: 2 },
},
}, },
}, },
{ {
@@ -61,7 +66,6 @@ export const defaultGame: () => SerializedGame = () => ({
pins: [ pins: [
{ {
resource: 'plank', resource: 'plank',
rate: 1,
link: { target: 'sawmill.1', resource: 'plank' }, link: { target: 'sawmill.1', resource: 'plank' },
}, },
], ],
@@ -69,6 +73,10 @@ export const defaultGame: () => SerializedGame = () => ({
outputs: { outputs: {
pins: [], pins: [],
}, },
rules: {
cycleLength: 1,
inputRates: { plank: 1 },
},
}, },
}, },
], ],
+18 -13
View File
@@ -31,14 +31,14 @@ export class Game {
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]));
for (const node of this.nodes) for (const node of this.nodes)
node.behaviour.produce(deltaTime); node.behaviour.tick(deltaTime);
for (const node of this.nodes) for (const node of this.nodes)
node.behaviour.pull(deltaTime, (id) => behavioursById.get(id)); node.behaviour.pull((id) => behavioursById.get(id));
} }
public reset() { public reset() {
this.time = Decimal.dZero; this.time = Decimal.dZero;
for (const node of this.nodes) for (const node of this.nodes)
node.behaviour.reset(); node.behaviour.reset();
} }
@@ -48,30 +48,35 @@ export class Game {
} }
} }
export type SerializedConverterInput = { export type SerializedInput = {
resource: Resource; resource: Resource;
rate: DecimalSource;
link: { link: {
target: SerializedNodeBehaviour['id'], target: SerializedNodeBehaviour['id'],
resource: Resource, resource: Resource,
} }
} }
export type SerializedConverterOutput = { export type SerializedOutput = {
resource: Resource; resource: Resource;
rate: DecimalSource;
} }
export type SerializedConverterInputs = { export type SerializedInputs = {
pins: SerializedConverterInput[]; pins: SerializedInput[];
} }
export type SerializedConverterOutputs = { export type SerializedOutputs = {
pins: SerializedConverterOutput[]; pins: SerializedOutput[];
} }
export type SerializedConverter = { export type SerializedConverter = {
kind: 'converter'; kind: 'converter';
cycleTime?: DecimalSource;
storage?: Partial<Record<Resource, DecimalSource>>;
rules: {
cycleLength: DecimalSource;
inputRates?: Partial<Record<Resource, DecimalSource>>;
outputRates?: Partial<Record<Resource, DecimalSource>>;
}
} }
export type SerializedStorage = { export type SerializedStorage = {
@@ -80,8 +85,8 @@ export type SerializedStorage = {
export type SerializedNodeBehaviour = { export type SerializedNodeBehaviour = {
id: string; id: string;
inputs: SerializedConverterInputs; inputs: SerializedInputs;
outputs: SerializedConverterOutputs; outputs: SerializedOutputs;
} & (SerializedConverter | SerializedStorage); } & (SerializedConverter | SerializedStorage);
export type SerializedNode = { export type SerializedNode = {