behaviour refactoring. storage beahavior added

This commit is contained in:
azykov@mail.ru
2026-09-03 12:31:23 +03:00
parent 557d22517a
commit f4c1eedf62
10 changed files with 158 additions and 120 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ export const GameCanvas = function ({
<div id="world" ref={canvasRef}> <div id="world" ref={canvasRef}>
{ {
graphNodes.map((graphNode) => 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); const target = graphNodes.find((gn) => gn.entity.id === link.target);
return target && ( return target && (
<LinkLine <LinkLine
+10 -19
View File
@@ -1,47 +1,38 @@
import Decimal, { DecimalSource } from "break_eternity.js"; import Decimal, { DecimalSource } from "break_eternity.js";
import { BehaviourDisplayInfo, BehaviourInputs, BehaviourOutputs, BehaviourSocket, Resource, SerializedBehaviour } from "../../types"; import { BehaviourDisplayInfo, BehaviourLink, BehaviourSocket, Resource, SerializedBehaviour } from "../../types";
export abstract class Behaviour { export abstract class Behaviour {
public readonly id: string; public readonly id: string;
public readonly kind: string; public readonly kind: string;
public readonly inputs: BehaviourInputs; public readonly inputLinks: BehaviourLink[];
public readonly outputs: BehaviourOutputs;
constructor(source: SerializedBehaviour) { constructor(source: SerializedBehaviour) {
this.id = source.id; this.id = source.id;
this.kind = source.kind; this.kind = source.kind;
this.outputs = { this.inputLinks = source.inputLinks.pins;
pins: source.outputs.pins.map((output) => ({
resource: output.resource,
})),
}
this.inputs = {
pins: source.inputs.pins.map((input) => ({
resource: input.resource,
link: input.link,
})),
}
} }
public getDisplayInfo(): BehaviourDisplayInfo { public getDisplayInfo(): BehaviourDisplayInfo {
return { return {
storages: {}, storages: {},
links: this.inputs.pins.map((input) => input.link),
sockets: [ sockets: [
...this.inputs.pins.map((input, index) => ({ ...this.inputs.map((input, index) => ({
type: 'input', type: 'input',
index, index,
resource: input.resource, resource: input,
} satisfies BehaviourSocket)), } satisfies BehaviourSocket)),
...this.outputs.pins.map((output, index) => ({ ...this.outputs.map((output, index) => ({
type: 'output', type: 'output',
index, index,
resource: output.resource, resource: output,
} satisfies BehaviourSocket)), } 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. */ /** 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; public abstract take(resource: Resource, amount: DecimalSource): Decimal;
+21 -11
View File
@@ -30,6 +30,14 @@ export class Converter extends Behaviour {
this.outputRates[res as Resource] = new Decimal(rate); 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 * 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 * 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(); const reserveCycles = idealCycleTime.div(this.cycleLength).ceil();
let newCycleTime = idealCycleTime; let newCycleTime = idealCycleTime;
for (const input of this.inputs.pins) { for (const [res, rate] of Object.entries(this.inputRates) as [Resource, Decimal][]) {
const rate = this.inputRates[input.resource] ?? Decimal.dZero;
if (rate.eq(0)) if (rate.eq(0))
continue; continue;
const target = rate.mul(reserveCycles); 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); const shortfall = target.sub(have).max(0);
if (shortfall.gt(0)) { if (shortfall.gt(0)) {
const source = resolve(input.link.target); const link = this.inputLinks.find((l) => l.resource === res);
if (source) if (link) {
this.give(input.resource, source.take(input.link.resource, shortfall)); 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)); 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(); const affordableCycles = this.cycleTime.div(this.cycleLength).floor();
if (affordableCycles.gt(0)) { if (affordableCycles.gt(0)) {
for (const input of this.inputs.pins) for (const [res, rate] of Object.entries(this.inputRates) as [Resource, Decimal][])
this.take(input.resource, (this.inputRates[input.resource] ?? Decimal.dZero).mul(affordableCycles)); this.take(res, rate.mul(affordableCycles));
for (const output of this.outputs.pins) for (const [res, rate] of Object.entries(this.outputRates) as [Resource, Decimal][])
this.give(output.resource, (this.outputRates[output.resource] ?? Decimal.dZero).mul(affordableCycles)); this.give(res, rate.mul(affordableCycles));
this.cycleTime = this.cycleTime.sub(affordableCycles.mul(this.cycleLength)); this.cycleTime = this.cycleTime.sub(affordableCycles.mul(this.cycleLength));
} }
} }
+78
View File
@@ -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<Resources>;
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<Resources> {
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]);
}
}
+20 -16
View File
@@ -1,7 +1,8 @@
import { defaultGame } from "../prefabs/game"; import { defaultGame } from "../prefabs/game";
import { SerializedBehaviour, SerializedGame } from "../types"; import { SerializedBehaviour, SerializedConverter, SerializedGame, SerializedStorage } from "../types";
import { Behaviour } from "./behaviours/base"; import { Behaviour } from "./behaviours/base";
import { Converter } from "./behaviours/converter"; import { Converter } from "./behaviours/converter";
import { Storage } from "./behaviours/storage";
import { Game } from "./game"; import { Game } from "./game";
export const saveParams = { export const saveParams = {
@@ -9,33 +10,34 @@ export const saveParams = {
} }
export function serializeBehaviour(behaviour: Behaviour): SerializedBehaviour { export function serializeBehaviour(behaviour: Behaviour): SerializedBehaviour {
const base = {
id: behaviour.id,
kind: behaviour.kind,
inputLinks: { pins: behaviour.inputLinks },
};
switch (behaviour.kind) { switch (behaviour.kind) {
case 'converter': { case 'converter': {
const converter = behaviour as Converter; const converter = behaviour as Converter;
return { return {
id: converter.id, ...base,
kind: 'converter',
cycleTime: converter.cycleTime, cycleTime: converter.cycleTime,
storage: converter.getStorage(), 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: { rules: {
cycleLength: converter.cycleLength, cycleLength: converter.cycleLength,
inputRates: converter.inputRates, inputRates: converter.inputRates,
outputRates: converter.outputRates, 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: default:
throw new Error('Unknown behaviour'); throw new Error('Unknown behaviour');
} }
@@ -45,6 +47,8 @@ export function deserializeBehaviour(value: SerializedBehaviour): Behaviour {
switch (value.kind) { switch (value.kind) {
case 'converter': case 'converter':
return new Converter(value); return new Converter(value);
case 'storage':
return new Storage(value);
default: default:
throw new Error('Unknown behaviour'); throw new Error('Unknown behaviour');
} }
+16 -36
View File
@@ -9,19 +9,7 @@ export const defaultGame: () => SerializedGame = () => ({
behaviour: { behaviour: {
id: 'forest.1', id: 'forest.1',
kind: 'converter', kind: 'converter',
inputs: { inputLinks: { pins: [] },
pins: [],
},
outputs: {
pins: [
{
resource: 'tree',
},
{
resource: 'stone',
},
],
},
rules: { rules: {
cycleLength: 1, cycleLength: 1,
outputRates: { tree: 1, stone: 0.1 }, outputRates: { tree: 1, stone: 0.1 },
@@ -34,21 +22,14 @@ export const defaultGame: () => SerializedGame = () => ({
behaviour: { behaviour: {
id: 'sawmill.1', id: 'sawmill.1',
kind: 'converter', kind: 'converter',
inputs: { inputLinks: {
pins: [ pins: [
{ {
resource: 'tree', resource: 'tree',
link: { target: 'forest.1', resource: 'tree' }, target: 'forest.1',
}, },
] ]
}, },
outputs: {
pins: [
{
resource: 'wood',
},
],
},
rules: { rules: {
cycleLength: 1.8, cycleLength: 1.8,
inputRates: { tree: 1 }, inputRates: { tree: 1 },
@@ -57,26 +38,25 @@ export const defaultGame: () => SerializedGame = () => ({
}, },
}, },
{ {
id: 'burner', id: 'shop',
name: 'Магазин досок', name: 'Магазин',
behaviour: { behaviour: {
id: 'burner.1', id: 'shop.1',
kind: 'converter', kind: 'storage',
inputs: { inputLinks: {
pins: [ pins: [
{ {
resource: 'wood', resource: 'wood',
link: { target: 'sawmill.1', resource: 'wood' }, target: 'sawmill.1',
}, },
], {
}, resource: 'stone',
outputs: { target: 'forest.1',
pins: [], }
}, ]
rules: {
cycleLength: 1,
inputRates: { wood: 1 },
}, },
acceptsResources: ['wood', 'stone'],
items: {},
}, },
}, },
], ],
-16
View File
@@ -15,25 +15,9 @@ export type BehaviourLink = {
resource: Resource; 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 = { export type BehaviourDisplayInfo = {
storages: Record<string, Resources>; storages: Record<string, Resources>;
sockets: BehaviourSocket[]; sockets: BehaviourSocket[];
links: BehaviourLink[];
[x: string]: unknown; [x: string]: unknown;
} }
+9 -18
View File
@@ -2,24 +2,13 @@ import { Unit } from './unit';
import { DecimalSource } from "break_eternity.js"; import { DecimalSource } from "break_eternity.js";
import { Resource } from "./resource"; import { Resource } from "./resource";
export type SerializedInput = { export type SerializedInputLink = {
resource: Resource; target: SerializedBehaviour['id'],
link: { resource: Resource,
target: SerializedBehaviour['id'],
resource: Resource,
}
} }
export type SerializedOutput = { export type SerializedInputLinks = {
resource: Resource; pins: SerializedInputLink[];
}
export type SerializedInputs = {
pins: SerializedInput[];
}
export type SerializedOutputs = {
pins: SerializedOutput[];
} }
export type SerializedConverter = { export type SerializedConverter = {
@@ -35,12 +24,14 @@ export type SerializedConverter = {
export type SerializedStorage = { export type SerializedStorage = {
kind: 'storage', kind: 'storage',
capacity?: DecimalSource;
acceptsResources: Resource[];
items: Partial<Record<Resource, DecimalSource>>;
} }
export type SerializedBehaviour = { export type SerializedBehaviour = {
id: string; id: string;
inputs: SerializedInputs; inputLinks: SerializedInputLinks;
outputs: SerializedOutputs;
} & (SerializedConverter | SerializedStorage); } & (SerializedConverter | SerializedStorage);
export type SerializedNode = { export type SerializedNode = {
+1 -1
View File
@@ -30,7 +30,7 @@ export async function build2dNodes(units: Unit[], area: { width: number, height:
height: NODE_HEIGHT, height: NODE_HEIGHT,
})), })),
edges: units.flatMap((node) => edges: units.flatMap((node) =>
node.behaviour.getDisplayInfo().links.map((link, idx) => ({ node.behaviour.inputLinks.map((link, idx) => ({
id: `${node.behaviour.id}->${link.target}.${idx}`, id: `${node.behaviour.id}->${link.target}.${idx}`,
sources: [node.behaviour.id], sources: [node.behaviour.id],
targets: [link.target], targets: [link.target],
+2 -2
View File
@@ -10,8 +10,8 @@ export function orderByDependency(behaviours: Behaviour[]): Behaviour[] {
if (visited.has(behaviour.id)) if (visited.has(behaviour.id))
return; return;
visited.add(behaviour.id); visited.add(behaviour.id);
for (const input of behaviour.inputs.pins) { for (const inputLink of behaviour.inputLinks) {
const dependency = byId.get(input.link.target); const dependency = byId.get(inputLink.target);
if (dependency) if (dependency)
visit(dependency); visit(dependency);
} }