project structure refactoring
This commit is contained in:
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Topbar from './components/Topbar';
|
import Topbar from './components/Topbar';
|
||||||
import { GameCanvas } from './components/GameCanvas';
|
import { GameCanvas } from './components/GameCanvas';
|
||||||
import { Game } from './types';
|
|
||||||
import { deserialize, load, save } from './engine/saveLoad';
|
import { deserialize, load, save } from './engine/saveLoad';
|
||||||
import { defaultGame } from './prefabs/game';
|
import { defaultGame } from './prefabs/game';
|
||||||
|
import { Game } from './engine/game';
|
||||||
|
|
||||||
export type Selection = { kind: 'node'; id: string } | null;
|
export type Selection = { kind: 'node'; id: string } | null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Selection } from '../App';
|
import type { Selection } from '../App';
|
||||||
import NodeCard from './NodeCard';
|
import NodeCard from './NodeCard';
|
||||||
import { Game } from '../types';
|
|
||||||
import { build2dNodes, Node2D } from '../ui/graph';
|
import { build2dNodes, Node2D } from '../ui/graph';
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { LinkLine } from './LinkLine';
|
import { LinkLine } from './LinkLine';
|
||||||
|
import { Game } from '../engine/game';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
game: Game;
|
game: Game;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Game, Resource } from "../types";
|
import { Game } from "../engine/game";
|
||||||
|
import { Resource } from "../types";
|
||||||
import { Node2D } from "../ui/graph";
|
import { Node2D } from "../ui/graph";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { Game, Resource } from '../types';
|
import { Resource } from '../types';
|
||||||
import { Node2D } from '../ui/graph';
|
import { Node2D } from '../ui/graph';
|
||||||
import { ResourceDescriptions } from '../ui/resource';
|
import { ResourceDescriptions } from '../ui/resource';
|
||||||
|
import { Game } from '../engine/game';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
game: Game;
|
game: Game;
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import type { Topology, SimNode, SimEdge, NodeKind, Behavior } from '../engine';
|
|
||||||
import type { Selection } from '../App';
|
|
||||||
import NodeEditor from './NodeEditor';
|
|
||||||
import EdgeEditor from './EdgeEditor';
|
|
||||||
import { Game, SerializedNodeBehaviour } from '../types';
|
|
||||||
|
|
||||||
const BEHAVIOR_DEFAULTS: SerializedNodeBehaviour[] = [
|
|
||||||
{
|
|
||||||
kind: 'converter',
|
|
||||||
inputs: [],
|
|
||||||
outputs: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
game: Game;
|
|
||||||
selected: Selection;
|
|
||||||
onSelectNode: (id: string) => void;
|
|
||||||
onSelectEdge: (id: string) => void;
|
|
||||||
onAddNode: (kind: NodeKind) => void;
|
|
||||||
onAddEdge: () => void;
|
|
||||||
onRemoveNode: (id: string) => void;
|
|
||||||
onRemoveEdge: (id: string) => void;
|
|
||||||
onUpdateNode: (id: string, patch: Partial<SimNode>) => void;
|
|
||||||
onUpdateNodeBehavior: (id: string, behavior: Behavior) => void;
|
|
||||||
onUpdateEdge: (id: string, patch: Partial<SimEdge>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Sidebar(props: Props) {
|
|
||||||
const { game: topology, selected } = props;
|
|
||||||
const selectedNode = selected?.kind === 'node' ? topology.nodes.find((n) => n.id === selected.id) : undefined;
|
|
||||||
const selectedEdge = selected?.kind === 'edge' ? topology.edges.find((e) => e.id === selected.id) : undefined;
|
|
||||||
|
|
||||||
function handleAddNodeChange(e: ChangeEvent<HTMLSelectElement>) {
|
|
||||||
const kind = e.target.value as NodeKind | '';
|
|
||||||
if (kind) props.onAddNode(kind);
|
|
||||||
e.target.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sidebar">
|
|
||||||
<div className="sidebar__section">
|
|
||||||
<h4>Узлы</h4>
|
|
||||||
<ul className="sidebar__list">
|
|
||||||
{topology.nodes.map((n) => (
|
|
||||||
<li
|
|
||||||
key={n.id}
|
|
||||||
className={selected?.kind === 'node' && selected.id === n.id ? 'is-active' : ''}
|
|
||||||
onClick={() => props.onSelectNode(n.id)}
|
|
||||||
>
|
|
||||||
{n.label} ({n.id})
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<select value="" onChange={handleAddNodeChange}>
|
|
||||||
<option value="">+ добавить узел…</option>
|
|
||||||
<option value="source">Источник</option>
|
|
||||||
<option value="storage">Склад</option>
|
|
||||||
<option value="converter">Конвертер</option>
|
|
||||||
<option value="consumer">Потребитель</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sidebar__section">
|
|
||||||
<h4>Связи</h4>
|
|
||||||
<ul className="sidebar__list">
|
|
||||||
{topology.edges.map((e) => (
|
|
||||||
<li
|
|
||||||
key={e.id}
|
|
||||||
className={selected?.kind === 'edge' && selected.id === e.id ? 'is-active' : ''}
|
|
||||||
onClick={() => props.onSelectEdge(e.id)}
|
|
||||||
>
|
|
||||||
{e.from} → {e.to} [{e.resource}]
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<button className="btn btn--ghost" style={{ marginTop: 8 }} onClick={props.onAddEdge}>
|
|
||||||
+ добавить связь
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sidebar__section sidebar__section--grow">
|
|
||||||
<h4>Редактор</h4>
|
|
||||||
<div id="sidebar-content">
|
|
||||||
{!selected && (
|
|
||||||
<p className="hint">
|
|
||||||
Выберите узел или связь в списке слева либо кликните по объекту на схеме.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{selectedNode && (
|
|
||||||
<NodeEditor
|
|
||||||
node={selectedNode}
|
|
||||||
onUpdate={(patch) => props.onUpdateNode(selectedNode.id, patch)}
|
|
||||||
onUpdateBehavior={(b) => props.onUpdateNodeBehavior(selectedNode.id, b)}
|
|
||||||
onRemove={() => props.onRemoveNode(selectedNode.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedEdge && (
|
|
||||||
<EdgeEditor
|
|
||||||
edge={selectedEdge}
|
|
||||||
nodes={topology.nodes}
|
|
||||||
onUpdate={(patch) => props.onUpdateEdge(selectedEdge.id, patch)}
|
|
||||||
onRemove={() => props.onRemoveEdge(selectedEdge.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||||
|
import { BehaviourDisplayInfo, BehaviourInputs, BehaviourOutputs, BehaviourSocket, Resource, SerializedBehaviour } from "../../types";
|
||||||
|
|
||||||
|
export abstract class Behaviour {
|
||||||
|
public readonly id: string;
|
||||||
|
public readonly kind: string;
|
||||||
|
public readonly inputs: BehaviourInputs;
|
||||||
|
public readonly outputs: BehaviourOutputs;
|
||||||
|
|
||||||
|
constructor(source: SerializedBehaviour) {
|
||||||
|
this.id = source.id;
|
||||||
|
this.kind = source.kind;
|
||||||
|
this.outputs = {
|
||||||
|
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 {
|
||||||
|
return {
|
||||||
|
storages: {},
|
||||||
|
links: this.inputs.pins.map((input) => input.link),
|
||||||
|
sockets: [
|
||||||
|
...this.inputs.pins.map((input, index) => ({
|
||||||
|
type: 'input',
|
||||||
|
index,
|
||||||
|
resource: input.resource,
|
||||||
|
} satisfies BehaviourSocket)),
|
||||||
|
...this.outputs.pins.map((output, index) => ({
|
||||||
|
type: 'output',
|
||||||
|
index,
|
||||||
|
resource: output.resource,
|
||||||
|
} 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 reset(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances this behaviour by `deltaTime`. Must pull whatever inputs it needs (via `resolve`)
|
||||||
|
* before advancing its own cycle, so that a cycle only completes once enough input was actually
|
||||||
|
* available — otherwise it stalls and tries again on a later tick.
|
||||||
|
*/
|
||||||
|
abstract tick(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void;
|
||||||
|
}
|
||||||
@@ -1,99 +1,6 @@
|
|||||||
import Decimal, { DecimalSource } from "break_eternity.js";
|
import Decimal, { DecimalSource } from "break_eternity.js";
|
||||||
import { Resource, Resources, SerializedConverter, SerializedNodeBehaviour as SerializedBehaviour, SerializedNodeBehaviour } from "../../types";
|
import { BehaviourDisplayInfo, Resource, Resources, SerializedBehaviour, SerializedConverter } from "../../types";
|
||||||
|
import { Behaviour } from "./base";
|
||||||
export type BehaviourResourcePin = {
|
|
||||||
resource: Resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BehaviourSocket = {
|
|
||||||
type: 'input' | 'output';
|
|
||||||
index: number; // within its type
|
|
||||||
resource: Resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BehaviourLink = {
|
|
||||||
target: Behaviour['id'],
|
|
||||||
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<string, Resources>;
|
|
||||||
sockets: BehaviourSocket[];
|
|
||||||
links: BehaviourLink[];
|
|
||||||
|
|
||||||
[x: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class Behaviour {
|
|
||||||
public readonly id: string;
|
|
||||||
public readonly kind: string;
|
|
||||||
public readonly inputs: BehaviourInputs;
|
|
||||||
public readonly outputs: BehaviourOutputs;
|
|
||||||
|
|
||||||
constructor(source: SerializedBehaviour) {
|
|
||||||
this.id = source.id;
|
|
||||||
this.kind = source.kind;
|
|
||||||
this.outputs = {
|
|
||||||
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 {
|
|
||||||
return {
|
|
||||||
storages: {},
|
|
||||||
links: this.inputs.pins.map((input) => input.link),
|
|
||||||
sockets: [
|
|
||||||
...this.inputs.pins.map((input, index) => ({
|
|
||||||
type: 'input',
|
|
||||||
index,
|
|
||||||
resource: input.resource,
|
|
||||||
} satisfies BehaviourSocket)),
|
|
||||||
...this.outputs.pins.map((output, index) => ({
|
|
||||||
type: 'output',
|
|
||||||
index,
|
|
||||||
resource: output.resource,
|
|
||||||
} 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 reset(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advances this behaviour by `deltaTime`. Must pull whatever inputs it needs (via `resolve`)
|
|
||||||
* before advancing its own cycle, so that a cycle only completes once enough input was actually
|
|
||||||
* available — otherwise it stalls and tries again on a later tick.
|
|
||||||
*/
|
|
||||||
abstract tick(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Converter extends Behaviour {
|
export class Converter extends Behaviour {
|
||||||
protected readonly storage: Partial<Resources>;
|
protected readonly storage: Partial<Resources>;
|
||||||
@@ -102,7 +9,7 @@ export class Converter extends Behaviour {
|
|||||||
public readonly inputRates: Partial<Resources>;
|
public readonly inputRates: Partial<Resources>;
|
||||||
public readonly outputRates: Partial<Resources>;
|
public readonly outputRates: Partial<Resources>;
|
||||||
|
|
||||||
constructor(source: SerializedNodeBehaviour & SerializedConverter) {
|
constructor(source: SerializedBehaviour & SerializedConverter) {
|
||||||
if (source.kind !== 'converter')
|
if (source.kind !== 'converter')
|
||||||
throw new Error('Invalid source');
|
throw new Error('Invalid source');
|
||||||
super(source);
|
super(source);
|
||||||
@@ -198,12 +105,3 @@ export class Converter extends Behaviour {
|
|||||||
delete (this.storage[res as Resource]);
|
delete (this.storage[res as Resource]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// export class Storage extends Behaviour {
|
|
||||||
// public static type: string = 'storage';
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const behaviours = {
|
|
||||||
'converter': Converter,
|
|
||||||
// 'storage': ,
|
|
||||||
} satisfies Record<string, { new(source: never): Behaviour }>;
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Decimal from "break_eternity.js";
|
||||||
|
import { deserializeBehaviour } from "./saveLoad";
|
||||||
|
import { SerializedGame } from "../types";
|
||||||
|
import { Node } from '../types';
|
||||||
|
import { orderByDependency } from "../utils/behaviourSort";
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
public time: Decimal;
|
||||||
|
public readonly nodes: Node[];
|
||||||
|
|
||||||
|
constructor(source: SerializedGame) {
|
||||||
|
this.time = new Decimal(source.time);
|
||||||
|
this.nodes = source.nodes.map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
behaviour: deserializeBehaviour(node.behaviour),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public tick(deltaTime: number) {
|
||||||
|
this.time = this.time.add(deltaTime);
|
||||||
|
|
||||||
|
const behavioursById = new Map(this.nodes.map((node) => [node.behaviour.id, node.behaviour]));
|
||||||
|
const resolve = (id: string) => behavioursById.get(id);
|
||||||
|
|
||||||
|
// Producers must tick (and so fill their storage) before whatever pulls from them,
|
||||||
|
// so resources can flow all the way down a chain within a single tick.
|
||||||
|
for (const behaviour of orderByDependency(this.nodes.map((node) => node.behaviour)))
|
||||||
|
behaviour.tick(deltaTime, resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { defaultGame } from "../prefabs/game";
|
import { defaultGame } from "../prefabs/game";
|
||||||
import { Game, SerializedGame, SerializedNodeBehaviour } from "../types";
|
import { SerializedBehaviour, SerializedGame } from "../types";
|
||||||
import { Behaviour, behaviours, Converter } from "./functions";
|
import { Behaviour } from "./behaviours/base";
|
||||||
|
import { Converter } from "./behaviours/converter";
|
||||||
|
import { Game } from "./game";
|
||||||
|
|
||||||
export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviour {
|
export function serializeBehaviour(behaviour: Behaviour): SerializedBehaviour {
|
||||||
switch (behaviour.kind) {
|
switch (behaviour.kind) {
|
||||||
case 'converter': {
|
case 'converter': {
|
||||||
const converter = behaviour as Converter;
|
const converter = behaviour as Converter;
|
||||||
@@ -35,10 +37,10 @@ export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeBehaviour(value: SerializedNodeBehaviour): Behaviour {
|
export function deserializeBehaviour(value: SerializedBehaviour): Behaviour {
|
||||||
switch (value.kind) {
|
switch (value.kind) {
|
||||||
case 'converter':
|
case 'converter':
|
||||||
return new (behaviours.converter)(value);
|
return new Converter(value);
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown behaviour');
|
throw new Error('Unknown behaviour');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Resource, Resources } from "./resource";
|
||||||
|
|
||||||
|
export type BehaviourResourcePin = {
|
||||||
|
resource: Resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BehaviourSocket = {
|
||||||
|
type: 'input' | 'output';
|
||||||
|
index: number; // within its type
|
||||||
|
resource: Resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BehaviourLink = {
|
||||||
|
target: string,
|
||||||
|
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<string, Resources>;
|
||||||
|
sockets: BehaviourSocket[];
|
||||||
|
links: BehaviourLink[];
|
||||||
|
|
||||||
|
[x: string]: unknown;
|
||||||
|
}
|
||||||
+4
-126
@@ -1,126 +1,4 @@
|
|||||||
import Decimal, { DecimalSource } from "break_eternity.js";
|
export * from './behaviour';
|
||||||
import { Behaviour } from "../engine/functions";
|
export * from './node';
|
||||||
import { deserializeBehaviour } from "../engine/saveLoad";
|
export * from './resource';
|
||||||
|
export * from './serialization';
|
||||||
export type Resource = 'wood' | 'plank' | 'stone';
|
|
||||||
|
|
||||||
export type Resources = Record<Resource, Decimal>;
|
|
||||||
|
|
||||||
export type Node = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
behaviour: Behaviour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Orders behaviours so that whatever a behaviour's inputs link to comes before it (a dependency cycle just falls back to encounter order). */
|
|
||||||
function orderByDependency(behaviours: Behaviour[]): Behaviour[] {
|
|
||||||
const byId = new Map(behaviours.map((behaviour) => [behaviour.id, behaviour]));
|
|
||||||
const visited = new Set<string>();
|
|
||||||
const order: Behaviour[] = [];
|
|
||||||
|
|
||||||
function visit(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);
|
|
||||||
if (dependency)
|
|
||||||
visit(dependency);
|
|
||||||
}
|
|
||||||
order.push(behaviour);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const behaviour of behaviours)
|
|
||||||
visit(behaviour);
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Game {
|
|
||||||
public time: Decimal;
|
|
||||||
public readonly nodes: Node[];
|
|
||||||
|
|
||||||
constructor(source: SerializedGame) {
|
|
||||||
this.time = new Decimal(source.time);
|
|
||||||
this.nodes = source.nodes.map((node) => ({
|
|
||||||
id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
behaviour: deserializeBehaviour(node.behaviour),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public tick(deltaTime: number) {
|
|
||||||
this.time = this.time.add(deltaTime);
|
|
||||||
|
|
||||||
const behavioursById = new Map(this.nodes.map((node) => [node.behaviour.id, node.behaviour]));
|
|
||||||
const resolve = (id: string) => behavioursById.get(id);
|
|
||||||
|
|
||||||
// Producers must tick (and so fill their storage) before whatever pulls from them,
|
|
||||||
// so resources can flow all the way down a chain within a single tick.
|
|
||||||
for (const behaviour of orderByDependency(this.nodes.map((node) => node.behaviour)))
|
|
||||||
behaviour.tick(deltaTime, resolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 SerializedInput = {
|
|
||||||
resource: Resource;
|
|
||||||
link: {
|
|
||||||
target: SerializedNodeBehaviour['id'],
|
|
||||||
resource: Resource,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedOutput = {
|
|
||||||
resource: Resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedInputs = {
|
|
||||||
pins: SerializedInput[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedOutputs = {
|
|
||||||
pins: SerializedOutput[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedConverter = {
|
|
||||||
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 = {
|
|
||||||
kind: 'storage',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedNodeBehaviour = {
|
|
||||||
id: string;
|
|
||||||
inputs: SerializedInputs;
|
|
||||||
outputs: SerializedOutputs;
|
|
||||||
} & (SerializedConverter | SerializedStorage);
|
|
||||||
|
|
||||||
export type SerializedNode = {
|
|
||||||
id: Node['id'],
|
|
||||||
name: Node['name'],
|
|
||||||
behaviour: SerializedNodeBehaviour,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SerializedGame = {
|
|
||||||
time: DecimalSource;
|
|
||||||
nodes: SerializedNode[];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Behaviour } from "../engine/behaviours/base";
|
||||||
|
|
||||||
|
export type Node = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
behaviour: Behaviour;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Decimal from "break_eternity.js";
|
||||||
|
|
||||||
|
export type Resource = 'wood' | 'plank' | 'stone';
|
||||||
|
|
||||||
|
export type Resources = Record<Resource, Decimal>;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Node } from './node';
|
||||||
|
import { DecimalSource } from "break_eternity.js";
|
||||||
|
import { Resource } from "./resource";
|
||||||
|
|
||||||
|
export type SerializedInput = {
|
||||||
|
resource: Resource;
|
||||||
|
link: {
|
||||||
|
target: SerializedBehaviour['id'],
|
||||||
|
resource: Resource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedOutput = {
|
||||||
|
resource: Resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedInputs = {
|
||||||
|
pins: SerializedInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedOutputs = {
|
||||||
|
pins: SerializedOutput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedConverter = {
|
||||||
|
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 = {
|
||||||
|
kind: 'storage',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedBehaviour = {
|
||||||
|
id: string;
|
||||||
|
inputs: SerializedInputs;
|
||||||
|
outputs: SerializedOutputs;
|
||||||
|
} & (SerializedConverter | SerializedStorage);
|
||||||
|
|
||||||
|
export type SerializedNode = {
|
||||||
|
id: Node['id'],
|
||||||
|
name: Node['name'],
|
||||||
|
behaviour: SerializedBehaviour,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedGame = {
|
||||||
|
time: DecimalSource;
|
||||||
|
nodes: SerializedNode[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Behaviour } from "../engine/behaviours/base";
|
||||||
|
|
||||||
|
/** Orders behaviours so that whatever a behaviour's inputs link to comes before it (a dependency cycle just falls back to encounter order). */
|
||||||
|
export function orderByDependency(behaviours: Behaviour[]): Behaviour[] {
|
||||||
|
const byId = new Map(behaviours.map((behaviour) => [behaviour.id, behaviour]));
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const order: Behaviour[] = [];
|
||||||
|
|
||||||
|
function visit(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);
|
||||||
|
if (dependency)
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
order.push(behaviour);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const behaviour of behaviours)
|
||||||
|
visit(behaviour);
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user