Compare commits
3
Commits
2860bb837a
...
b9c0c0ee28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c0c0ee28 | ||
|
|
90501176ef | ||
|
|
d96a6e2e92 |
Generated
+7
@@ -8,6 +8,7 @@
|
|||||||
"name": "resource-graph-sim",
|
"name": "resource-graph-sim",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"break_eternity.js": "^2.1.3",
|
||||||
"elkjs": "^0.12.0",
|
"elkjs": "^0.12.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
@@ -1231,6 +1232,12 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/break_eternity.js": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/break_eternity.js/-/break_eternity.js-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-4tg4j0wc0lhaYAnOHubN5mAyHbhMfUI7adQLO8l/loKqtylZ/kHWp8WYqG2EC0TinSesKvpCi3XeVFcKRUBJsQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.8",
|
"version": "4.28.8",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"break_eternity.js": "^2.1.3",
|
||||||
"elkjs": "^0.12.0",
|
"elkjs": "^0.12.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
|
|||||||
+4
-10
@@ -15,13 +15,8 @@ export default function App() {
|
|||||||
// useEffect(() => saveTickCount(tickCount), [tickCount]);
|
// useEffect(() => saveTickCount(tickCount), [tickCount]);
|
||||||
|
|
||||||
function handleTick() {
|
function handleTick() {
|
||||||
game.tick(1.0 / 60);
|
game.tick(1.0 / 10);
|
||||||
// const nextTopology = cloneJSON(game);
|
setGame(game.clone());
|
||||||
// const nextBuffers = cloneJSON(buffers);
|
|
||||||
// tick(nextTopology, nextBuffers);
|
|
||||||
// setGame(nextTopology);
|
|
||||||
// setBuffers(nextBuffers);
|
|
||||||
// setTickCount((n) => n + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
@@ -31,8 +26,8 @@ export default function App() {
|
|||||||
if (!ok)
|
if (!ok)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
game.time = 0;
|
game.reset();
|
||||||
save(game);
|
setGame(game.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInitialize() {
|
function handleInitialize() {
|
||||||
@@ -43,7 +38,6 @@ export default function App() {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
setGame(deserialize(defaultGame()));
|
setGame(deserialize(defaultGame()));
|
||||||
save(game);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// function handleAddNode(kind: NodeKind) {
|
// function handleAddNode(kind: NodeKind) {
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ const NodeCard = forwardRef<HTMLDivElement, Props>(function NodeCard({ node2d, s
|
|||||||
<div className="node__kind">{node.behaviour.kind}</div>
|
<div className="node__kind">{node.behaviour.kind}</div>
|
||||||
{
|
{
|
||||||
Object.entries(info.storages).map(([name, storage]) => (
|
Object.entries(info.storages).map(([name, storage]) => (
|
||||||
<div className="node__resources">
|
<div className="node__resources" key={name}>
|
||||||
<div>{name}</div>
|
<div>{name}</div>
|
||||||
{
|
{
|
||||||
Object.entries(storage)
|
Object.entries(storage)
|
||||||
.map(([rt, v]) => `${ResourceDescriptions[rt as Resource].icon}${v}`).join(' ')
|
.map(([rt, v]) => `${ResourceDescriptions[rt as Resource].icon}${v.toStringWithDecimalPlaces(4)}`).join(' ')
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -73,6 +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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type Decimal from 'break_eternity.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
time: number;
|
time: Decimal;
|
||||||
onTick: () => void;
|
onTick: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onInitialize: () => void;
|
onInitialize: () => void;
|
||||||
@@ -13,7 +15,7 @@ export default function Topbar({ time, onTick, onReset, onInitialize }: Props) {
|
|||||||
<span>детерминированная тиковая симуляция</span>
|
<span>детерминированная тиковая симуляция</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="tick-counter">
|
<div className="tick-counter">
|
||||||
Время <b>{time}</b>
|
Время <b>{time.toStringWithDecimalPlaces(4)}</b>
|
||||||
</div>
|
</div>
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
<button className="btn" onClick={onInitialize}>
|
<button className="btn" onClick={onInitialize}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Resource, Resources, SerializedConverter, SerializedNodeBehaviour as SerializedBehaviour, SerializedNodeBehaviour, SerializedConverterOutput, SerializedConverterInput } from "../../types";
|
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||||
|
import { Resource, Resources, SerializedConverter, SerializedNodeBehaviour as SerializedBehaviour, SerializedNodeBehaviour } from "../../types";
|
||||||
|
|
||||||
export type BehaviourResourcePin = {
|
export type BehaviourResourcePin = {
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
@@ -41,13 +42,14 @@ 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;
|
||||||
this.kind = source.kind;
|
this.kind = source.kind;
|
||||||
this.outputs = {
|
this.outputs = {
|
||||||
pins: source.outputs.pins.map((input) => ({
|
pins: source.outputs.pins.map((output) => ({
|
||||||
resource: input.resource,
|
resource: output.resource,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
this.inputs = {
|
this.inputs = {
|
||||||
@@ -58,34 +60,69 @@ export abstract class Behaviour {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
for (const res of Object.keys(this.storage))
|
||||||
|
delete (this.storage[res as Resource]);
|
||||||
|
}
|
||||||
|
|
||||||
abstract getDisplayInfo(): BehaviourDisplayInfo;
|
abstract getDisplayInfo(): BehaviourDisplayInfo;
|
||||||
|
|
||||||
abstract tick(deltaTime: number): void;
|
/** 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;
|
||||||
|
}
|
||||||
|
|
||||||
export type ResourceUsageRule = {
|
protected give(resource: Resource, amount: DecimalSource): void {
|
||||||
resource: Resource;
|
this.storage[resource] = (this.storage[resource] ?? Decimal.dZero).add(amount);
|
||||||
speed: number; // per second
|
}
|
||||||
|
|
||||||
|
abstract produce(deltaTime: number): void;
|
||||||
|
|
||||||
|
abstract pull(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Converter extends Behaviour {
|
export class Converter extends Behaviour {
|
||||||
public outputRules: ResourceUsageRule[] = [];
|
public readonly inputRates: Partial<Resources>;
|
||||||
rules: { inputs: SerializedConverterInput[]; outputs: SerializedConverterOutput[]; };
|
public readonly outputRates: Partial<Resources>;
|
||||||
|
|
||||||
constructor(source: SerializedNodeBehaviour & SerializedConverter) {
|
constructor(source: SerializedNodeBehaviour & SerializedConverter) {
|
||||||
if (source.kind !== 'converter')
|
if (source.kind !== 'converter')
|
||||||
throw new Error('Invalid source');
|
throw new Error('Invalid source');
|
||||||
super(source);
|
super(source);
|
||||||
|
|
||||||
this.rules = {
|
this.inputRates = {};
|
||||||
inputs: source.inputs.pins,
|
for (const pin of source.inputs.pins)
|
||||||
outputs: source.outputs.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: {},
|
storages: { 'Буфер': { ...this.storage } as Resources },
|
||||||
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) => ({
|
||||||
@@ -101,10 +138,6 @@ export class Converter extends Behaviour {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(deltaTime: number): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export class Storage extends Behaviour {
|
// export class Storage extends Behaviour {
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import { defaultGame } from "../prefabs/game";
|
import { defaultGame } from "../prefabs/game";
|
||||||
import { Game, SerializedGame, SerializedNodeBehaviour } from "../types";
|
import { Game, SerializedGame, SerializedNodeBehaviour } from "../types";
|
||||||
import { Behaviour, behaviours } from "./functions";
|
import { Behaviour, behaviours, Converter } from "./functions";
|
||||||
|
|
||||||
export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviour {
|
export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviour {
|
||||||
switch (behaviour.kind) {
|
switch (behaviour.kind) {
|
||||||
case 'converter':
|
case 'converter': {
|
||||||
|
const converter = behaviour as Converter;
|
||||||
return {
|
return {
|
||||||
id: behaviour.id,
|
id: converter.id,
|
||||||
kind: 'converter',
|
kind: 'converter',
|
||||||
inputs: {
|
inputs: {
|
||||||
pins: behaviour.inputs.pins.map((pin) => ({
|
pins: converter.inputs.pins.map((pin) => ({
|
||||||
...pin,
|
...pin,
|
||||||
rate: 11,
|
rate: converter.inputRates[pin.resource] ?? 0,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
pins: behaviour.outputs.pins.map((pin) => ({
|
pins: converter.outputs.pins.map((pin) => ({
|
||||||
...pin,
|
...pin,
|
||||||
rate: 11,
|
rate: converter.outputRates[pin.resource] ?? 0,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown behaviour');
|
throw new Error('Unknown behaviour');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import type { Topology, Buffers } from './engine';
|
|
||||||
import { defaultTopology, resetBuffers } from './engine';
|
|
||||||
|
|
||||||
const KEY_TOPOLOGY = 'incsim.topology.v1';
|
|
||||||
const KEY_BUFFERS = 'incsim.buffers.v1';
|
|
||||||
const KEY_TICKS = 'incsim.tickcount.v1';
|
|
||||||
|
|
||||||
export function loadTopology(): Topology {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(KEY_TOPOLOGY);
|
|
||||||
if (raw) return JSON.parse(raw) as Topology;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Не удалось прочитать игру из localStorage', e);
|
|
||||||
}
|
|
||||||
const fresh = defaultTopology();
|
|
||||||
saveTopology(fresh);
|
|
||||||
return fresh;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveTopology(topology: Topology): void {
|
|
||||||
localStorage.setItem(KEY_TOPOLOGY, JSON.stringify(topology));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadBuffers(topology: Topology): Buffers {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(KEY_BUFFERS);
|
|
||||||
if (raw) return JSON.parse(raw) as Buffers;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Не удалось прочитать буферы из localStorage', e);
|
|
||||||
}
|
|
||||||
const fresh = resetBuffers(topology);
|
|
||||||
saveBuffers(fresh);
|
|
||||||
return fresh;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveBuffers(buffers: Buffers): void {
|
|
||||||
localStorage.setItem(KEY_BUFFERS, JSON.stringify(buffers));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadTickCount(): number {
|
|
||||||
const raw = localStorage.getItem(KEY_TICKS);
|
|
||||||
return raw ? Number(raw) || 0 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveTickCount(n: number): void {
|
|
||||||
localStorage.setItem(KEY_TICKS, String(n));
|
|
||||||
}
|
|
||||||
+25
-8
@@ -1,9 +1,10 @@
|
|||||||
|
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||||
import { Behaviour } from "../engine/functions";
|
import { Behaviour } from "../engine/functions";
|
||||||
import { deserializeBehaviour } from "../engine/saveLoad";
|
import { deserializeBehaviour } from "../engine/saveLoad";
|
||||||
|
|
||||||
export type Resource = 'wood' | 'plank' | 'stone';
|
export type Resource = 'wood' | 'plank' | 'stone';
|
||||||
|
|
||||||
export type Resources = Record<Resource, number>;
|
export type Resources = Record<Resource, Decimal>;
|
||||||
|
|
||||||
export type Node = {
|
export type Node = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,11 +13,11 @@ export type Node = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
public time: number;
|
public time: Decimal;
|
||||||
public readonly nodes: Node[];
|
public readonly nodes: Node[];
|
||||||
|
|
||||||
constructor(source: SerializedGame) {
|
constructor(source: SerializedGame) {
|
||||||
this.time = source.time;
|
this.time = new Decimal(source.time);
|
||||||
this.nodes = source.nodes.map((node) => ({
|
this.nodes = source.nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
@@ -25,15 +26,31 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public tick(deltaTime: number) {
|
public tick(deltaTime: number) {
|
||||||
this.time += deltaTime;
|
this.time = this.time.add(deltaTime);
|
||||||
|
|
||||||
|
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.tick(deltaTime);
|
node.behaviour.produce(deltaTime);
|
||||||
|
for (const node of this.nodes)
|
||||||
|
node.behaviour.pull(deltaTime, (id) => behavioursById.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.time = Decimal.dZero;
|
||||||
|
for (const node of this.nodes)
|
||||||
|
node.behaviour.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shallow copy with a new identity, so React sees a change after in-place mutations like `tick`. */
|
||||||
|
public clone(): Game {
|
||||||
|
return Object.assign(Object.create(Game.prototype), this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedConverterInput = {
|
export type SerializedConverterInput = {
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
rate: number;
|
rate: DecimalSource;
|
||||||
link: {
|
link: {
|
||||||
target: SerializedNodeBehaviour['id'],
|
target: SerializedNodeBehaviour['id'],
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
@@ -42,7 +59,7 @@ export type SerializedConverterInput = {
|
|||||||
|
|
||||||
export type SerializedConverterOutput = {
|
export type SerializedConverterOutput = {
|
||||||
resource: Resource;
|
resource: Resource;
|
||||||
rate: number;
|
rate: DecimalSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedConverterInputs = {
|
export type SerializedConverterInputs = {
|
||||||
@@ -74,6 +91,6 @@ export type SerializedNode = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedGame = {
|
export type SerializedGame = {
|
||||||
time: number;
|
time: DecimalSource;
|
||||||
nodes: SerializedNode[];
|
nodes: SerializedNode[];
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user