classes refactoring, converter input pull amount fix
This commit is contained in:
@@ -1,66 +1,68 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { Resource } from '../types';
|
import { Resource } from '../types';
|
||||||
import { Node2D } from '../ui/graph';
|
import { GraphNode } from '../ui/graph';
|
||||||
import { ResourceDescriptions } from '../ui/resource';
|
import { ResourceDescriptions } from '../ui/resource';
|
||||||
import { Game } from '../engine/game';
|
import { Game } from '../engine/game';
|
||||||
|
import { ConverterCardInfo } from './ConverterCardInfo';
|
||||||
|
import { Converter } from '../engine/behaviours/converter';
|
||||||
|
import { Behaviour } from '../engine/behaviours/base';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
game: Game;
|
game: Game;
|
||||||
node2d: Node2D;
|
position: GraphNode['position'];
|
||||||
|
behaviour: Behaviour,
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onSocketRef?: (type: 'input' | 'output', index: number, el: HTMLDivElement | null) => void;
|
onSocketRef?: (type: 'input' | 'output', index: number, el: HTMLDivElement | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeCard = forwardRef<HTMLDivElement, Props>(function NodeCard({ node2d, selected, onSelect, onSocketRef }, ref) {
|
export const BehaviourCard = forwardRef<HTMLDivElement, Props>(function NodeCard({ position, behaviour, selected, onSelect, onSocketRef }, ref) {
|
||||||
const node = node2d.node;
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
onSelect();
|
onSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = node.behaviour.getDisplayInfo();
|
const info = behaviour.getDisplayInfo();
|
||||||
const inputSockets = info.sockets.filter((s) => s.type === 'input');
|
const inputSockets = info.sockets.filter((s) => s.type === 'input');
|
||||||
const outputSockets = info.sockets.filter((s) => s.type === 'output');
|
const outputSockets = info.sockets.filter((s) => s.type === 'output');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`node node--${node.behaviour.kind}${selected ? ' is-selected' : ''}`}
|
className={`node node--${behaviour.kind}${selected ? ' is-selected' : ''}`}
|
||||||
style={{ left: node2d.position.x, top: node2d.position.y }}
|
style={{ left: position.x, top: position.y }}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div className="node__sockets node__sockets--input">
|
<div className="unit__sockets unit__sockets--input">
|
||||||
{
|
{
|
||||||
inputSockets.map((socket) => (
|
inputSockets.map((socket) => (
|
||||||
<div
|
<div
|
||||||
key={`input.${socket.index}`}
|
key={`input.${socket.index}`}
|
||||||
ref={(el) => onSocketRef?.('input', socket.index, el)}
|
ref={(el) => onSocketRef?.('input', socket.index, el)}
|
||||||
className="node__socket"
|
className="unit__socket"
|
||||||
style={{ background: ResourceDescriptions[socket.resource].color }}
|
style={{ background: ResourceDescriptions[socket.resource].color }}
|
||||||
title={`${ResourceDescriptions[socket.resource].label} (вход)`}
|
title={`${ResourceDescriptions[socket.resource].label} (вход)`}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="node__sockets node__sockets--output">
|
<div className="unit__sockets unit__sockets--output">
|
||||||
{
|
{
|
||||||
outputSockets.map((socket) => (
|
outputSockets.map((socket) => (
|
||||||
<div
|
<div
|
||||||
key={`output.${socket.index}`}
|
key={`output.${socket.index}`}
|
||||||
ref={(el) => onSocketRef?.('output', socket.index, el)}
|
ref={(el) => onSocketRef?.('output', socket.index, el)}
|
||||||
className="node__socket"
|
className="unit__socket"
|
||||||
style={{ background: ResourceDescriptions[socket.resource].color }}
|
style={{ background: ResourceDescriptions[socket.resource].color }}
|
||||||
title={`${ResourceDescriptions[socket.resource].label} (выход)`}
|
title={`${ResourceDescriptions[socket.resource].label} (выход)`}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="node__id">#{node.id}</div>
|
<div className="unit__id">#{behaviour.id}</div>
|
||||||
<div className="node__title">{node.name}</div>
|
<div className="unit__kind">{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" key={name}>
|
<div className="unit__resources" key={name}>
|
||||||
<div>{name}</div>
|
<div>{name}</div>
|
||||||
{
|
{
|
||||||
Object.entries(storage)
|
Object.entries(storage)
|
||||||
@@ -69,14 +71,15 @@ const NodeCard = forwardRef<HTMLDivElement, Props>(function NodeCard({ node2d, s
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
<div className="node__links">
|
<div className="unit__links">
|
||||||
{
|
{
|
||||||
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: '8px', whiteSpace: 'pre'}}>{JSON.stringify(info.raw, undefined, 4)}</div>
|
{
|
||||||
|
behaviour.kind === 'coverter' && <ConverterCardInfo converter={behaviour as Converter} />
|
||||||
|
}
|
||||||
|
<div className="unit__debug" style={{ fontSize: '8px', whiteSpace: 'pre' }}>{JSON.stringify(info.raw, undefined, 4)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default NodeCard;
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Converter } from "../engine/behaviours/converter";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
converter: Converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConverterCardInfo = function ({ converter }: Props) {
|
||||||
|
return <>
|
||||||
|
<div className="unit__converter__progress">
|
||||||
|
<div className="unit__converter__progress_bar" style={{ width: `${converter.cycleTime.div(converter.cycleLength).toFixed(2)}%` }} />
|
||||||
|
<div className="unit__converter__progress_info">{converter.cycleTime.toFixed(4)} of {converter.cycleLength.toFixed(4)}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Selection } from '../App';
|
import type { Selection } from '../App';
|
||||||
import NodeCard from './NodeCard';
|
import { BehaviourCard } from './BehaviourCard';
|
||||||
import { build2dNodes, Node2D } from '../ui/graph';
|
import { build2dNodes, GraphNode } 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';
|
import { Game } from '../engine/game';
|
||||||
@@ -35,7 +35,7 @@ export const GameCanvas = function ({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [nodes2d, setNodes2d] = useState<Node2D[]>([]);
|
const [graphNodes, setNodes2d] = useState<GraphNode[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -61,70 +61,71 @@ export const GameCanvas = function ({
|
|||||||
|
|
||||||
const nextCenters: Record<string, { x: number, y: number }> = {};
|
const nextCenters: Record<string, { x: number, y: number }> = {};
|
||||||
const nextSocketCenters: Record<string, { x: number, y: number }> = {};
|
const nextSocketCenters: Record<string, { x: number, y: number }> = {};
|
||||||
for (const node2d of nodes2d) {
|
for (const graphNode of graphNodes) {
|
||||||
const el = nodeElsRef.current.get(node2d.id);
|
const el = nodeElsRef.current.get(graphNode.entity.id);
|
||||||
if (el) nextCenters[node2d.id] = centerOf(el);
|
if (el) nextCenters[graphNode.entity.id] = centerOf(el);
|
||||||
|
|
||||||
for (const socket of node2d.node.behaviour.getDisplayInfo().sockets) {
|
for (const socket of graphNode.entity.getDisplayInfo().sockets) {
|
||||||
const socketKey = `${node2d.id}.${socket.type}.${socket.index}`;
|
const socketKey = `${graphNode.entity.id}.${socket.type}.${socket.index}`;
|
||||||
const socketEl = socketElsRef.current.get(socketKey);
|
const socketEl = socketElsRef.current.get(socketKey);
|
||||||
if (socketEl) nextSocketCenters[socketKey] = centerOf(socketEl);
|
if (socketEl) nextSocketCenters[socketKey] = centerOf(socketEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCenters(nextCenters);
|
setCenters(nextCenters);
|
||||||
setSocketCenters(nextSocketCenters);
|
setSocketCenters(nextSocketCenters);
|
||||||
}, [nodes2d]);
|
}, [graphNodes]);
|
||||||
|
|
||||||
function withCenter(node2d: Node2D): Node2D {
|
function withCenter(graphNode: GraphNode): GraphNode {
|
||||||
const center = centers[node2d.id];
|
const center = centers[graphNode.entity.id];
|
||||||
return center ? { ...node2d, position: center } : node2d;
|
return center ? { ...graphNode, position: center } : graphNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withSocket(node2d: Node2D, type: 'input' | 'output', resource: string): Node2D {
|
function withSocket(graphNode: GraphNode, type: 'input' | 'output', resource: string): GraphNode {
|
||||||
const socket = node2d.node.behaviour.getDisplayInfo().sockets
|
const socket = graphNode.entity.getDisplayInfo().sockets
|
||||||
.find((s) => s.type === type && s.resource === resource);
|
.find((s) => s.type === type && s.resource === resource);
|
||||||
const center = socket && socketCenters[`${node2d.id}.${type}.${socket.index}`];
|
const center = socket && socketCenters[`${graphNode.entity.id}.${type}.${socket.index}`];
|
||||||
return center ? { ...node2d, position: center } : withCenter(node2d);
|
return center ? { ...graphNode, position: center } : withCenter(graphNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="world-wrap">
|
<div className="world-wrap">
|
||||||
<div id="world" ref={canvasRef}>
|
<div id="world" ref={canvasRef}>
|
||||||
{
|
{
|
||||||
nodes2d.map((node2d) =>
|
graphNodes.map((graphNode) =>
|
||||||
node2d.node.behaviour.getDisplayInfo().links.map((link, idx) => {
|
graphNode.entity.getDisplayInfo().links.map((link, idx) => {
|
||||||
const target = nodes2d.find((n2d) => n2d.id === link.target);
|
const target = graphNodes.find((gn) => gn.entity.id === link.target);
|
||||||
return target && (
|
return target && (
|
||||||
<LinkLine
|
<LinkLine
|
||||||
game={game}
|
game={game}
|
||||||
key={`${node2d.id}.${idx}`}
|
key={`${graphNode.entity.id}.${idx}`}
|
||||||
from={{ node: withSocket(node2d, 'input', link.resource), resource: link.resource }}
|
from={{ node: withSocket(graphNode, 'input', link.resource), resource: link.resource }}
|
||||||
to={{ node: withSocket(target, 'output', link.resource), resource: link.resource }}
|
to={{ node: withSocket(target, 'output', link.resource), resource: link.resource }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
nodes2d.map((node2d) => (
|
graphNodes.map((graphNode) => (
|
||||||
<NodeCard
|
<BehaviourCard
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (el)
|
if (el)
|
||||||
nodeElsRef.current.set(node2d.id, el);
|
nodeElsRef.current.set(graphNode.entity.id, el);
|
||||||
else
|
else
|
||||||
nodeElsRef.current.delete(node2d.id);
|
nodeElsRef.current.delete(graphNode.entity.id);
|
||||||
}}
|
}}
|
||||||
onSocketRef={(type, index, el) => {
|
onSocketRef={(type, index, el) => {
|
||||||
const socketKey = `${node2d.id}.${type}.${index}`;
|
const socketKey = `${graphNode.entity.id}.${type}.${index}`;
|
||||||
if (el)
|
if (el)
|
||||||
socketElsRef.current.set(socketKey, el);
|
socketElsRef.current.set(socketKey, el);
|
||||||
else
|
else
|
||||||
socketElsRef.current.delete(socketKey);
|
socketElsRef.current.delete(socketKey);
|
||||||
}}
|
}}
|
||||||
game={game}
|
game={game}
|
||||||
key={node2d.id}
|
key={graphNode.entity.id}
|
||||||
node2d={node2d}
|
position={graphNode.position}
|
||||||
selected={selected?.kind === 'node' && selected.id === node2d.id}
|
behaviour={graphNode.entity}
|
||||||
onSelect={() => onSelectNode(node2d.id)}
|
selected={selected?.kind === 'node' && selected.id === graphNode.entity.id}
|
||||||
|
onSelect={() => onSelectNode(graphNode.entity.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Game } from "../engine/game";
|
import { Game } from "../engine/game";
|
||||||
import { Resource } from "../types";
|
import { Resource } from "../types";
|
||||||
import { Node2D } from "../ui/graph";
|
import { GraphNode } from "../ui/graph";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
game: Game,
|
game: Game,
|
||||||
from: { node: Node2D, resource: Resource },
|
from: { node: GraphNode, resource: Resource },
|
||||||
to: { node: Node2D, resource: Resource },
|
to: { node: GraphNode, resource: Resource },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkLine = function ({ from, to }: Props) {
|
export const LinkLine = function ({ from, to }: Props) {
|
||||||
const markerId = `node-line-arrow-${from.node.id}-${to.node.id}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const markerId = `node-line-arrow-${from.node.entity.id}-${to.node.entity.id}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
|
||||||
const fromSocket = from.node.node.behaviour.getDisplayInfo().sockets.find((s) => s.type === 'input' && s.resource == from.resource);
|
const fromSocket = from.node.entity.getDisplayInfo().sockets.find((s) => s.type === 'input' && s.resource == from.resource);
|
||||||
const toSocket = to.node.node.behaviour.getDisplayInfo().sockets.find((s) => s.type === 'output' && s.resource == to.resource);
|
const toSocket = to.node.entity.getDisplayInfo().sockets.find((s) => s.type === 'output' && s.resource == to.resource);
|
||||||
|
|
||||||
if (!fromSocket || !toSocket)
|
if (!fromSocket || !toSocket)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -31,37 +31,40 @@ export class Converter extends Behaviour {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advances time, pulls as much input as is available toward however many cycles that time
|
* A converter grabs a full batch of each input the moment enough is available — reserving
|
||||||
* would allow, then completes only as many full cycles as the input actually on hand supports.
|
* ahead for however many cycles (including the one currently underway) the elapsed time could
|
||||||
* Any leftover cycle time is kept for later — a starved converter simply stalls until its
|
* reach, rather than trickling resources in proportionally as time passes. If the source can't
|
||||||
* inputs catch up, and then bursts through the backlog once they do.
|
* supply the full batch, `cycleTime` only advances as far as what was actually secured
|
||||||
|
* justifies: a converter can't be "cooking" a batch it doesn't have the ingredients for, so it
|
||||||
|
* stalls proportionally to whatever is actually missing rather than free-running on elapsed
|
||||||
|
* time alone.
|
||||||
*/
|
*/
|
||||||
public tick(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void {
|
public tick(deltaTime: number, resolve: (id: string) => Behaviour | undefined): void {
|
||||||
this.cycleTime = this.cycleTime.add(deltaTime);
|
const idealCycleTime = this.cycleTime.add(deltaTime);
|
||||||
const wantedCycles = this.cycleTime.div(this.cycleLength).floor();
|
const reserveCycles = idealCycleTime.div(this.cycleLength).ceil();
|
||||||
|
let newCycleTime = idealCycleTime;
|
||||||
|
|
||||||
if (wantedCycles.gt(0)) {
|
|
||||||
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 have = this.storage[input.resource] ?? Decimal.dZero;
|
|
||||||
const stillNeeded = rate.mul(wantedCycles).sub(have).max(0);
|
|
||||||
if (stillNeeded.gt(0))
|
|
||||||
this.give(input.resource, source.take(input.link.resource, stillNeeded));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let affordableCycles = wantedCycles;
|
|
||||||
for (const input of this.inputs.pins) {
|
for (const input of this.inputs.pins) {
|
||||||
const rate = this.inputRates[input.resource] ?? Decimal.dZero;
|
const rate = this.inputRates[input.resource] ?? Decimal.dZero;
|
||||||
if (rate.eq(0))
|
if (rate.eq(0))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
const target = rate.mul(reserveCycles);
|
||||||
const have = this.storage[input.resource] ?? Decimal.dZero;
|
const have = this.storage[input.resource] ?? Decimal.dZero;
|
||||||
affordableCycles = affordableCycles.min(have.div(rate).floor());
|
const shortfall = target.sub(have).max(0);
|
||||||
|
if (shortfall.gt(0)) {
|
||||||
|
const source = resolve(input.link.target);
|
||||||
|
if (source)
|
||||||
|
this.give(input.resource, source.take(input.link.resource, shortfall));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const haveNow = this.storage[input.resource] ?? Decimal.dZero;
|
||||||
|
newCycleTime = newCycleTime.min(haveNow.mul(this.cycleLength).div(rate));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cycleTime = newCycleTime;
|
||||||
|
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 input of this.inputs.pins)
|
||||||
this.take(input.resource, (this.inputRates[input.resource] ?? Decimal.dZero).mul(affordableCycles));
|
this.take(input.resource, (this.inputRates[input.resource] ?? Decimal.dZero).mul(affordableCycles));
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import Decimal from "break_eternity.js";
|
import Decimal from "break_eternity.js";
|
||||||
import { deserializeBehaviour } from "./saveLoad";
|
import { deserializeBehaviour } from "./saveLoad";
|
||||||
import { SerializedGame } from "../types";
|
import { SerializedGame } from "../types";
|
||||||
import { Node } from '../types';
|
import { Unit } from '../types';
|
||||||
import { orderByDependency } from "../utils/behaviourSort";
|
import { orderByDependency } from "../utils/behaviourSort";
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
public time: Decimal;
|
public time: Decimal;
|
||||||
public readonly nodes: Node[];
|
public readonly nodes: Unit[];
|
||||||
|
|
||||||
constructor(source: SerializedGame) {
|
constructor(source: SerializedGame) {
|
||||||
this.time = new Decimal(source.time);
|
this.time = new Decimal(source.time);
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { Behaviour } from "./behaviours/base";
|
|||||||
import { Converter } from "./behaviours/converter";
|
import { Converter } from "./behaviours/converter";
|
||||||
import { Game } from "./game";
|
import { Game } from "./game";
|
||||||
|
|
||||||
|
export const saveParams = {
|
||||||
|
path: 'default',
|
||||||
|
}
|
||||||
|
|
||||||
export function serializeBehaviour(behaviour: Behaviour): SerializedBehaviour {
|
export function serializeBehaviour(behaviour: Behaviour): SerializedBehaviour {
|
||||||
switch (behaviour.kind) {
|
switch (behaviour.kind) {
|
||||||
case 'converter': {
|
case 'converter': {
|
||||||
@@ -72,12 +76,12 @@ export function deserializeFromString(value: string): Game {
|
|||||||
const KEY_GAME = 'incgane';
|
const KEY_GAME = 'incgane';
|
||||||
|
|
||||||
export function save(game: Game) {
|
export function save(game: Game) {
|
||||||
localStorage.setItem(KEY_GAME, serializeToString(game));
|
localStorage.setItem(`${saveParams.path}_${KEY_GAME}`, serializeToString(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function load(): Game {
|
export function load(): Game {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(KEY_GAME);
|
const raw = localStorage.getItem(`${saveParams.path}_${KEY_GAME}`);
|
||||||
if (raw)
|
if (raw)
|
||||||
return deserializeFromString(raw);
|
return deserializeFromString(raw);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+9
-9
@@ -202,7 +202,7 @@ select {
|
|||||||
.node--converter { border-left-color: var(--accent-converter); }
|
.node--converter { border-left-color: var(--accent-converter); }
|
||||||
.node--consumer { border-left-color: var(--accent-consumer); }
|
.node--consumer { border-left-color: var(--accent-consumer); }
|
||||||
|
|
||||||
.node__id {
|
.unit__id {
|
||||||
font-family: var(--display);
|
font-family: var(--display);
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -212,7 +212,7 @@ select {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node__title {
|
.unit__title {
|
||||||
font-family: var(--display);
|
font-family: var(--display);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
@@ -222,7 +222,7 @@ select {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node__kind {
|
.unit__kind {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -230,7 +230,7 @@ select {
|
|||||||
margin: 2px 0 6px;
|
margin: 2px 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node__resources {
|
.unit__resources {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
background: #171613;
|
background: #171613;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -240,7 +240,7 @@ select {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node__links {
|
.unit__links {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
background: #171613;
|
background: #171613;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -250,7 +250,7 @@ select {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node__sockets {
|
.unit__sockets {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -260,10 +260,10 @@ select {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.node__sockets--input { left: -5px; }
|
.unit__sockets--input { left: -5px; }
|
||||||
.node__sockets--output { right: -5px; }
|
.unit__sockets--output { right: -5px; }
|
||||||
|
|
||||||
.node__socket {
|
.unit__socket {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
export * from './behaviour';
|
export * from './behaviour';
|
||||||
export * from './node';
|
export * from './unit';
|
||||||
export * from './resource';
|
export * from './resource';
|
||||||
export * from './serialization';
|
export * from './serialization';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Node } from './node';
|
import { Unit } from './unit';
|
||||||
import { DecimalSource } from "break_eternity.js";
|
import { DecimalSource } from "break_eternity.js";
|
||||||
import { Resource } from "./resource";
|
import { Resource } from "./resource";
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ export type SerializedBehaviour = {
|
|||||||
} & (SerializedConverter | SerializedStorage);
|
} & (SerializedConverter | SerializedStorage);
|
||||||
|
|
||||||
export type SerializedNode = {
|
export type SerializedNode = {
|
||||||
id: Node['id'],
|
id: Unit['id'],
|
||||||
name: Node['name'],
|
name: Unit['name'],
|
||||||
behaviour: SerializedBehaviour,
|
behaviour: SerializedBehaviour,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Behaviour } from "../engine/behaviours/base";
|
import { Behaviour } from "../engine/behaviours/base";
|
||||||
|
|
||||||
export type Node = {
|
export type Unit = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
behaviour: Behaviour;
|
behaviour: Behaviour;
|
||||||
+14
-13
@@ -1,19 +1,20 @@
|
|||||||
import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js";
|
import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js";
|
||||||
import { Node } from "../types";
|
import { Unit } from "../types";
|
||||||
|
import { Behaviour } from "../engine/behaviours/base";
|
||||||
|
|
||||||
const elk = new ELK();
|
const elk = new ELK();
|
||||||
|
|
||||||
const NODE_WIDTH = 100;
|
const UNIT_WIDTH = 100;
|
||||||
const NODE_HEIGHT = 90;
|
const NODE_HEIGHT = 90;
|
||||||
|
|
||||||
export type Node2D = {
|
export type GraphNode = {
|
||||||
id: Node['id'];
|
|
||||||
position: { x: number, y: number };
|
position: { x: number, y: number };
|
||||||
node: Node;
|
kind: 'behaviour';
|
||||||
|
entity: Behaviour;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function build2dNodes(nodes: Node[], area: { width: number, height: number, padding: number }): Promise<Node2D[]> {
|
export async function build2dNodes(units: Unit[], area: { width: number, height: number, padding: number }): Promise<GraphNode[]> {
|
||||||
const nodesById = new Map(nodes.map((node) => [node.behaviour.id, node]));
|
const entitiesById = new Map(units.map((unit) => [unit.behaviour.id, unit.behaviour]));
|
||||||
|
|
||||||
const graph: ElkNode = {
|
const graph: ElkNode = {
|
||||||
id: "root",
|
id: "root",
|
||||||
@@ -23,12 +24,12 @@ export async function build2dNodes(nodes: Node[], area: { width: number, height:
|
|||||||
"elk.spacing.nodeNode": "60",
|
"elk.spacing.nodeNode": "60",
|
||||||
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
||||||
},
|
},
|
||||||
children: nodes.map((node) => ({
|
children: units.map((unit) => ({
|
||||||
id: node.behaviour.id,
|
id: unit.behaviour.id,
|
||||||
width: NODE_WIDTH,
|
width: UNIT_WIDTH,
|
||||||
height: NODE_HEIGHT,
|
height: NODE_HEIGHT,
|
||||||
})),
|
})),
|
||||||
edges: nodes.flatMap((node) =>
|
edges: units.flatMap((node) =>
|
||||||
node.behaviour.getDisplayInfo().links.map((link, idx) => ({
|
node.behaviour.getDisplayInfo().links.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],
|
||||||
@@ -40,11 +41,11 @@ export async function build2dNodes(nodes: Node[], area: { width: number, height:
|
|||||||
const layout = await elk.layout(graph);
|
const layout = await elk.layout(graph);
|
||||||
|
|
||||||
return (layout.children ?? []).map((child) => ({
|
return (layout.children ?? []).map((child) => ({
|
||||||
id: child.id,
|
|
||||||
position: {
|
position: {
|
||||||
x: (child.x ?? 0) + area.padding,
|
x: (child.x ?? 0) + area.padding,
|
||||||
y: (child.y ?? 0) + area.padding,
|
y: (child.y ?? 0) + area.padding,
|
||||||
},
|
},
|
||||||
node: nodesById.get(child.id)!,
|
kind: 'behaviour',
|
||||||
|
entity: entitiesById.get(child.id)!,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user