behaviour refactoring. storage beahavior added
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 (link) {
|
||||||
|
const source = resolve(link.target);
|
||||||
if (source)
|
if (source)
|
||||||
this.give(input.resource, source.take(input.link.resource, shortfall));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
link: {
|
|
||||||
target: SerializedBehaviour['id'],
|
target: SerializedBehaviour['id'],
|
||||||
resource: Resource,
|
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
@@ -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],
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user