Repository navigation
Expand file tree
/
Copy pathsystem.js
More file actions
144 lines (131 loc) · 5.58 KB
/
Copy pathsystem.js
File metadata and controls
144 lines (131 loc) · 5.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// The energy system built on the table: nodes (one per scanned card) and links
// between them. Pure logic, no three.js, so it runs in Node tests too.
//
// Link types follow from the assets: a link carries 'electricity' (or 'heat') when one
// side produces it and the other consumes it; the producer is the source of the flow.
// a, b: asset definitions from components.json ({ produces, consumes }) or null.
// Returns [{ carrier, forward }]: forward = true means the flow goes a → b.
// When both directions work for a carrier (e.g. two batteries), a → b is used.
export function resolveLink(a, b) {
if (!a || !b) return [];
const result = [];
const carriers = new Set([...(a.produces || []), ...(a.consumes || []), ...(b.produces || []), ...(b.consumes || [])]);
for (const carrier of carriers) {
const ab = (a.produces || []).includes(carrier) && (b.consumes || []).includes(carrier);
const ba = (b.produces || []).includes(carrier) && (a.consumes || []).includes(carrier);
if (ab) result.push({ carrier, forward: true });
else if (ba) result.push({ carrier, forward: false });
}
return result;
}
export class EnergySystem {
// config: the parsed components.json
constructor(config) {
this.config = config;
this.nodes = []; // { id, colour, type (asset key or null), x, z }
this.links = []; // { id, carrier, from, to, power }
this.history = []; // undo stack
this.nextId = 1;
}
assetOf(node) {
return node.type ? this.config.assets[node.type] ?? null : null;
}
labelOf(node) {
return this.assetOf(node)?.label ?? `${node.colour} card`;
}
node(id) {
return this.nodes.find((n) => n.id === id) ?? null;
}
// cards: [{ colour, x, z }] in table-local metres. Returns the new nodes.
addCards(cards) {
const added = cards.map((c) => ({
id: this.nextId++,
colour: c.colour,
type: this.config.cards?.[c.colour] ?? null,
x: c.x,
z: c.z,
}));
if (!added.length) return added;
this.nodes.push(...added);
this.history.push({ kind: 'add', nodeIds: added.map((n) => n.id) });
return added;
}
// Manual mode: one asset of the given type (key in config.assets) at (x, z),
// table-local metres. colour only sets the ring/label colour (null = white).
// Undone like a scan. Returns the new node.
addNode(type, x, z, colour = null) {
if (!this.config.assets?.[type]) throw new Error(`unknown asset "${type}"`);
const node = { id: this.nextId++, colour, type, x, z };
this.nodes.push(node);
this.history.push({ kind: 'add', nodeIds: [node.id] });
return node;
}
// Kind of the step undo() would undo next: 'add' | 'link' | 'unlink' | null.
lastStepKind() {
return this.history.at(-1)?.kind ?? null;
}
linksBetween(idA, idB) {
return this.links.filter((l) => (l.from === idA && l.to === idB) || (l.from === idB && l.to === idA));
}
// Click on two nodes. Removes their links if they are connected already,
// otherwise adds the links their assets allow.
// Returns { action: 'added' | 'removed' | 'refused', links, reason? }.
connect(idA, idB) {
const a = this.node(idA), b = this.node(idB);
if (!a || !b || idA === idB) return { action: 'refused', links: [], reason: 'pick two different assets' };
const existing = this.linksBetween(idA, idB);
if (existing.length) {
this.links = this.links.filter((l) => !existing.includes(l));
this.history.push({ kind: 'unlink', links: existing });
return { action: 'removed', links: existing };
}
const assetA = this.assetOf(a), assetB = this.assetOf(b);
if (!assetA || !assetB) {
const missing = !assetA ? a : b;
return { action: 'refused', links: [], reason: `the ${missing.colour} card has no asset in components.json` };
}
const options = resolveLink(assetA, assetB);
if (!options.length) {
return { action: 'refused', links: [], reason: `${assetA.label} and ${assetB.label} can't be connected` };
}
const added = options.map(({ carrier, forward }) => {
const from = forward ? a : b;
const to = forward ? b : a;
return { id: this.nextId++, carrier, from: from.id, to: to.id, power: this.assetOf(from).power_kW ?? 1 };
});
this.links.push(...added);
this.history.push({ kind: 'link', linkIds: added.map((l) => l.id) });
return { action: 'added', links: added };
}
// Undoes the last scan, link or unlink. Returns a short description, or null.
undo() {
const step = this.history.pop();
if (!step) return null;
if (step.kind === 'add') {
const ids = new Set(step.nodeIds);
this.nodes = this.nodes.filter((n) => !ids.has(n.id));
this.links = this.links.filter((l) => !ids.has(l.from) && !ids.has(l.to));
return `removed ${ids.size} asset${ids.size === 1 ? '' : 's'}`;
}
if (step.kind === 'link') {
const ids = new Set(step.linkIds);
this.links = this.links.filter((l) => !ids.has(l.id));
return 'removed the last connection';
}
// unlink: put the links back (their nodes may be gone if they were undone meanwhile)
const restore = step.links.filter((l) => this.node(l.from) && this.node(l.to));
this.links.push(...restore);
return 'restored the connection';
}
clear() {
this.nodes = [];
this.links = [];
this.history = [];
}
}
// The card colour that components.json maps to this asset type, or null. Manual assets
// use it for their ring and label, so an asset looks the same in both modes.
export function colourForType(config, type) {
for (const [colour, t] of Object.entries(config?.cards ?? {})) if (t === type) return colour;
return null;
}