@adatechnology/game-interactables 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -2
- package/dist/index.d.ts +831 -0
- package/dist/index.global.js +1765 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1740 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1740 @@
|
|
|
1
|
+
// src/colliders.ts
|
|
2
|
+
var volume = (b) => (b.u1 - b.u0) * (b.v1 - b.v0) * (b.y1 - b.y0);
|
|
3
|
+
var overlap = (a0, a1, b0, b1) => Math.max(0, Math.min(a1, b1) - Math.max(a0, b0));
|
|
4
|
+
function tryMerge(a, b, gap, slack) {
|
|
5
|
+
if (a.u0 - gap > b.u1 || b.u0 - gap > a.u1) return null;
|
|
6
|
+
if (a.v0 - gap > b.v1 || b.v0 - gap > a.v1) return null;
|
|
7
|
+
if (a.y0 - gap > b.y1 || b.y0 - gap > a.y1) return null;
|
|
8
|
+
const u0 = Math.min(a.u0, b.u0);
|
|
9
|
+
const u1 = Math.max(a.u1, b.u1);
|
|
10
|
+
const v0 = Math.min(a.v0, b.v0);
|
|
11
|
+
const v1 = Math.max(a.v1, b.v1);
|
|
12
|
+
const y0 = Math.min(a.y0, b.y0);
|
|
13
|
+
const y1 = Math.max(a.y1, b.y1);
|
|
14
|
+
const hull = (u1 - u0) * (v1 - v0) * (y1 - y0);
|
|
15
|
+
const common = overlap(a.u0, a.u1, b.u0, b.u1) * overlap(a.v0, a.v1, b.v0, b.v1) * overlap(a.y0, a.y1, b.y0, b.y1);
|
|
16
|
+
const real = volume(a) + volume(b) - common;
|
|
17
|
+
if (hull > real * (1 + slack) + 1e-9) return null;
|
|
18
|
+
return { ids: [...a.ids, ...b.ids], object: a.object, u0, u1, v0, v1, y0, y1 };
|
|
19
|
+
}
|
|
20
|
+
function mergeColliders(list, options = {}) {
|
|
21
|
+
const gap = options.gap ?? 0.02;
|
|
22
|
+
const slack = options.slack ?? 0.1;
|
|
23
|
+
const groups = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const c of list) {
|
|
25
|
+
const key = `${c.object}|${Math.round(c.ry * 1e3)}`;
|
|
26
|
+
const g = groups.get(key);
|
|
27
|
+
if (g) g.push(c);
|
|
28
|
+
else groups.set(key, [c]);
|
|
29
|
+
}
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const g of groups.values()) {
|
|
32
|
+
const ry = g[0]?.ry ?? 0;
|
|
33
|
+
const cs = Math.cos(ry);
|
|
34
|
+
const sn = Math.sin(ry);
|
|
35
|
+
let boxes = g.map((c) => {
|
|
36
|
+
const u = c.cx * cs - c.cz * sn;
|
|
37
|
+
const v = c.cx * sn + c.cz * cs;
|
|
38
|
+
return { ids: [c.id], object: c.object, u0: u - c.hw, u1: u + c.hw, v0: v - c.hd, v1: v + c.hd, y0: c.y0, y1: c.y1 };
|
|
39
|
+
});
|
|
40
|
+
let changed = boxes.length > 1;
|
|
41
|
+
while (changed) {
|
|
42
|
+
changed = false;
|
|
43
|
+
outer: for (let i = 0; i < boxes.length; i++) {
|
|
44
|
+
for (let j = i + 1; j < boxes.length; j++) {
|
|
45
|
+
const m = tryMerge(boxes[i], boxes[j], gap, slack);
|
|
46
|
+
if (!m) continue;
|
|
47
|
+
boxes = boxes.filter((_, k) => k !== i && k !== j);
|
|
48
|
+
boxes.push(m);
|
|
49
|
+
changed = true;
|
|
50
|
+
break outer;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const b of boxes) {
|
|
55
|
+
const u = (b.u0 + b.u1) / 2;
|
|
56
|
+
const v = (b.v0 + b.v1) / 2;
|
|
57
|
+
out.push({
|
|
58
|
+
id: b.ids.length === 1 ? b.ids[0] : `${b.ids[0]}+${b.ids.length - 1}`,
|
|
59
|
+
object: b.object,
|
|
60
|
+
cx: u * cs + v * sn,
|
|
61
|
+
cz: -u * sn + v * cs,
|
|
62
|
+
hw: (b.u1 - b.u0) / 2,
|
|
63
|
+
hd: (b.v1 - b.v0) / 2,
|
|
64
|
+
ry,
|
|
65
|
+
y0: b.y0,
|
|
66
|
+
y1: b.y1
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/rng.ts
|
|
74
|
+
function hashString(text) {
|
|
75
|
+
let h = 2166136261;
|
|
76
|
+
for (let i = 0; i < text.length; i++) {
|
|
77
|
+
h ^= text.charCodeAt(i);
|
|
78
|
+
h = Math.imul(h, 16777619);
|
|
79
|
+
}
|
|
80
|
+
return h >>> 0;
|
|
81
|
+
}
|
|
82
|
+
function rng(seed = 1) {
|
|
83
|
+
let a = (typeof seed === "number" ? Math.floor(seed) >>> 0 : hashString(seed)) || 2654435769;
|
|
84
|
+
return () => {
|
|
85
|
+
a = a + 1831565813 >>> 0;
|
|
86
|
+
let t = a;
|
|
87
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
88
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
89
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/loot.ts
|
|
94
|
+
function fitsSize(size, max) {
|
|
95
|
+
const eps = 1e-9;
|
|
96
|
+
if (size[1] > max[1] + eps) return false;
|
|
97
|
+
return size[0] <= max[0] + eps && size[2] <= max[2] + eps || size[2] <= max[0] + eps && size[0] <= max[2] + eps;
|
|
98
|
+
}
|
|
99
|
+
function fitsSlot(type, slot, items) {
|
|
100
|
+
const def = items?.[type];
|
|
101
|
+
return def ? fitsSize(def.size, slot.max) : true;
|
|
102
|
+
}
|
|
103
|
+
function generateLoot(tables, tableId, slots, seed, items) {
|
|
104
|
+
const table = tables?.[tableId];
|
|
105
|
+
const out = slots.map(() => null);
|
|
106
|
+
if (!table) return out;
|
|
107
|
+
const rand = rng(seed);
|
|
108
|
+
slots.forEach((slot, i) => {
|
|
109
|
+
const roll = rand();
|
|
110
|
+
const pick = rand();
|
|
111
|
+
const q = rand();
|
|
112
|
+
if (roll >= table.fill) return;
|
|
113
|
+
const candidates = table.entries.filter((e) => e.weight > 0 && fitsSlot(e.type, slot, items));
|
|
114
|
+
const total = candidates.reduce((s, e) => s + e.weight, 0);
|
|
115
|
+
if (total <= 0) return;
|
|
116
|
+
let r = pick * total;
|
|
117
|
+
let chosen = candidates[candidates.length - 1];
|
|
118
|
+
for (const e of candidates) {
|
|
119
|
+
r -= e.weight;
|
|
120
|
+
if (r < 0) {
|
|
121
|
+
chosen = e;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const [lo, hi] = chosen.qty ?? [1, 1];
|
|
126
|
+
const def = items?.[chosen.type];
|
|
127
|
+
const stackMax = def ? def.stack ?? 1 : Number.POSITIVE_INFINITY;
|
|
128
|
+
const qty = Math.max(1, Math.min(stackMax, lo + Math.floor(q * (hi - lo + 1))));
|
|
129
|
+
out[i] = { type: chosen.type, qty };
|
|
130
|
+
});
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/locale.ts
|
|
135
|
+
var locales = {
|
|
136
|
+
en: {
|
|
137
|
+
"prompt.open": "Open {name}",
|
|
138
|
+
"prompt.close": "Close {name}",
|
|
139
|
+
"prompt.turn_on": "Turn on {name}",
|
|
140
|
+
"prompt.turn_off": "Turn off {name}",
|
|
141
|
+
"prompt.take": "Take",
|
|
142
|
+
"prompt.sit": "Sit",
|
|
143
|
+
"prompt.lie_down": "Lie down",
|
|
144
|
+
"prompt.get_up": "Get up",
|
|
145
|
+
"prompt.light": "Light",
|
|
146
|
+
"prompt.change_channel": "Change channel",
|
|
147
|
+
"prompt.read": "Read",
|
|
148
|
+
"prompt.breaker_on": "Switch on {circuit}",
|
|
149
|
+
"prompt.breaker_off": "Switch off {circuit}",
|
|
150
|
+
"prompt.open_valve": "Open valve",
|
|
151
|
+
"prompt.close_valve": "Close valve",
|
|
152
|
+
no_power: "No power",
|
|
153
|
+
no_gas: "No gas",
|
|
154
|
+
no_water: "No water",
|
|
155
|
+
locked: "Locked",
|
|
156
|
+
requires_open: "Open it first",
|
|
157
|
+
already_lit: "Already lit",
|
|
158
|
+
needs_ignition: "Light it first",
|
|
159
|
+
turn_on_first: "Turn it on first",
|
|
160
|
+
empty: "Empty",
|
|
161
|
+
full: "Full"
|
|
162
|
+
},
|
|
163
|
+
"pt-BR": {
|
|
164
|
+
"prompt.open": "Abrir {name}",
|
|
165
|
+
"prompt.close": "Fechar {name}",
|
|
166
|
+
"prompt.turn_on": "Ligar {name}",
|
|
167
|
+
"prompt.turn_off": "Desligar {name}",
|
|
168
|
+
"prompt.take": "Pegar",
|
|
169
|
+
"prompt.sit": "Sentar",
|
|
170
|
+
"prompt.lie_down": "Deitar",
|
|
171
|
+
"prompt.get_up": "Levantar",
|
|
172
|
+
"prompt.light": "Acender",
|
|
173
|
+
"prompt.change_channel": "Trocar de canal",
|
|
174
|
+
"prompt.read": "Ler",
|
|
175
|
+
"prompt.breaker_on": "Ligar {circuit}",
|
|
176
|
+
"prompt.breaker_off": "Desligar {circuit}",
|
|
177
|
+
"prompt.open_valve": "Abrir registro",
|
|
178
|
+
"prompt.close_valve": "Fechar registro",
|
|
179
|
+
no_power: "Sem energia",
|
|
180
|
+
no_gas: "Sem g\xE1s",
|
|
181
|
+
no_water: "Sem \xE1gua",
|
|
182
|
+
locked: "Trancado",
|
|
183
|
+
requires_open: "Abra primeiro",
|
|
184
|
+
already_lit: "J\xE1 est\xE1 aceso",
|
|
185
|
+
needs_ignition: "Acenda primeiro",
|
|
186
|
+
turn_on_first: "Ligue primeiro",
|
|
187
|
+
empty: "Vazio",
|
|
188
|
+
full: "Cheio"
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function pickLocale(locale) {
|
|
192
|
+
if (!locale) return "en";
|
|
193
|
+
const l = locale.toLowerCase();
|
|
194
|
+
if (l === "pt-br" || l === "pt") return "pt-BR";
|
|
195
|
+
return "en";
|
|
196
|
+
}
|
|
197
|
+
function resolveStrings(locale, extra) {
|
|
198
|
+
const loc = pickLocale(locale);
|
|
199
|
+
return { ...locales.en, ...locales[loc], ...extra?.en ?? {}, ...loc === "en" ? {} : extra?.[loc] ?? {} };
|
|
200
|
+
}
|
|
201
|
+
function format(text, vars) {
|
|
202
|
+
if (!vars) return text;
|
|
203
|
+
return text.replace(/\{(\w+)\}/g, (m, k) => k in vars ? String(vars[k]) : m);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/math.ts
|
|
207
|
+
var DEFAULT_AXIS = [0, 1, 0];
|
|
208
|
+
function poseOf(motion, t) {
|
|
209
|
+
const tt = Math.max(0, Math.min(1, t));
|
|
210
|
+
const axis = motion.axis ?? DEFAULT_AXIS;
|
|
211
|
+
const pose = { position: [0, 0, 0], rotation: { axis: [axis[0], axis[1], axis[2]], angle: 0 }, scale: [1, 1, 1] };
|
|
212
|
+
switch (motion.kind) {
|
|
213
|
+
case "hinge":
|
|
214
|
+
case "rocker":
|
|
215
|
+
case "knob":
|
|
216
|
+
pose.rotation.angle = (motion.angle ?? Math.PI / 2) * tt;
|
|
217
|
+
break;
|
|
218
|
+
case "slide": {
|
|
219
|
+
const d = (motion.distance ?? 0.4) * tt;
|
|
220
|
+
pose.position = [axis[0] * d, axis[1] * d, axis[2] * d];
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case "scale": {
|
|
224
|
+
const k = 1 + ((motion.scale ?? 0.2) - 1) * tt;
|
|
225
|
+
pose.scale = [axis[0] !== 0 ? k : 1, axis[1] !== 0 ? k : 1, axis[2] !== 0 ? k : 1];
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return pose;
|
|
230
|
+
}
|
|
231
|
+
function rotY(v, ry) {
|
|
232
|
+
const c = Math.cos(ry);
|
|
233
|
+
const s = Math.sin(ry);
|
|
234
|
+
return [c * v[0] + s * v[2], v[1], -s * v[0] + c * v[2]];
|
|
235
|
+
}
|
|
236
|
+
function localToWorld(transform, v) {
|
|
237
|
+
const r = rotY(v, transform.ry);
|
|
238
|
+
return [transform.x + r[0], transform.y + r[1], transform.z + r[2]];
|
|
239
|
+
}
|
|
240
|
+
function worldToLocal(transform, v) {
|
|
241
|
+
return rotY([v[0] - transform.x, v[1] - transform.y, v[2] - transform.z], -transform.ry);
|
|
242
|
+
}
|
|
243
|
+
function rotateAxis(v, axis, angle) {
|
|
244
|
+
const len = Math.hypot(axis[0], axis[1], axis[2]) || 1;
|
|
245
|
+
const k = [axis[0] / len, axis[1] / len, axis[2] / len];
|
|
246
|
+
const c = Math.cos(angle);
|
|
247
|
+
const s = Math.sin(angle);
|
|
248
|
+
const dot = k[0] * v[0] + k[1] * v[1] + k[2] * v[2];
|
|
249
|
+
const cross = [k[1] * v[2] - k[2] * v[1], k[2] * v[0] - k[0] * v[2], k[0] * v[1] - k[1] * v[0]];
|
|
250
|
+
return [v[0] * c + cross[0] * s + k[0] * dot * (1 - c), v[1] * c + cross[1] * s + k[1] * dot * (1 - c), v[2] * c + cross[2] * s + k[2] * dot * (1 - c)];
|
|
251
|
+
}
|
|
252
|
+
function clamp(v, lo, hi) {
|
|
253
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
254
|
+
}
|
|
255
|
+
function dist3(a, b) {
|
|
256
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/world.ts
|
|
260
|
+
var DEFAULT_ACTOR = "player";
|
|
261
|
+
var DEFAULT_RADIUS = 1.2;
|
|
262
|
+
var AUTO_RANGE = 2;
|
|
263
|
+
var SMOKE_TAIL = 20;
|
|
264
|
+
var FLUSH_SECONDS = 2;
|
|
265
|
+
var ACTOR_HEIGHT = 1.7;
|
|
266
|
+
var PUSH_GAIN = 1.2;
|
|
267
|
+
var SWEEP_STEPS = 24;
|
|
268
|
+
var FLOW_DEFAULTS = { tap: 6, shower: 8, utility_sink: 6, flush: 0, hose: 10, filter: 1 };
|
|
269
|
+
function cloneStack(s) {
|
|
270
|
+
return s ? { type: s.type, qty: s.qty, ...s.data ? { data: JSON.parse(JSON.stringify(s.data)) } : {} } : null;
|
|
271
|
+
}
|
|
272
|
+
function humanize(id) {
|
|
273
|
+
const s = id.replace(/[_-]+/g, " ").trim();
|
|
274
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
275
|
+
}
|
|
276
|
+
function createWorld(options = {}) {
|
|
277
|
+
const seed = options.seed ?? 1;
|
|
278
|
+
const objs = /* @__PURE__ */ new Map();
|
|
279
|
+
const breakers = /* @__PURE__ */ new Map();
|
|
280
|
+
const seats = /* @__PURE__ */ new Map();
|
|
281
|
+
const plugs = /* @__PURE__ */ new Map();
|
|
282
|
+
let locale = options.locale ?? "en";
|
|
283
|
+
let table = resolveStrings(locale, options.strings);
|
|
284
|
+
let grid = options.grid ?? true;
|
|
285
|
+
let rain = options.rain ?? 0;
|
|
286
|
+
let wind = options.wind ? [options.wind[0], options.wind[1]] : [0, 0];
|
|
287
|
+
let defaultTank = null;
|
|
288
|
+
let defaultTankExplicit = false;
|
|
289
|
+
let time = 0;
|
|
290
|
+
function t(key, vars) {
|
|
291
|
+
return format(table[key] ?? key, vars);
|
|
292
|
+
}
|
|
293
|
+
function emit(e) {
|
|
294
|
+
options.onEvent?.(e);
|
|
295
|
+
}
|
|
296
|
+
function message(key) {
|
|
297
|
+
emit({ type: "message", key, text: t(key) });
|
|
298
|
+
}
|
|
299
|
+
function gid(o, local) {
|
|
300
|
+
return `${o.id}:${local}`;
|
|
301
|
+
}
|
|
302
|
+
function findGlobal(id, pick) {
|
|
303
|
+
for (const o of objs.values()) {
|
|
304
|
+
if (!id.startsWith(o.id + ":")) continue;
|
|
305
|
+
const local = id.slice(o.id.length + 1);
|
|
306
|
+
const value = pick(o, local);
|
|
307
|
+
if (value !== void 0) return { o, value, local };
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function objectOrThrow(id) {
|
|
312
|
+
const o = objs.get(id);
|
|
313
|
+
if (!o) throw new Error(`Unknown object "${id}"`);
|
|
314
|
+
return o;
|
|
315
|
+
}
|
|
316
|
+
function originOf(o) {
|
|
317
|
+
const tr = o.public.transform;
|
|
318
|
+
return [tr.x, tr.y, tr.z];
|
|
319
|
+
}
|
|
320
|
+
function toWorld(o, v) {
|
|
321
|
+
return localToWorld(o.public.transform, v);
|
|
322
|
+
}
|
|
323
|
+
function openThreshold(p) {
|
|
324
|
+
return Math.min(0.5, p.maxT * 0.9);
|
|
325
|
+
}
|
|
326
|
+
function isOpen(o, partId) {
|
|
327
|
+
if (!partId) return true;
|
|
328
|
+
const p = o.parts.get(partId);
|
|
329
|
+
if (!p) return true;
|
|
330
|
+
return p.maxT >= 0.1 && p.t >= openThreshold(p);
|
|
331
|
+
}
|
|
332
|
+
function targetIsOpen(p) {
|
|
333
|
+
return p.maxT >= 0.1 && p.target >= openThreshold(p);
|
|
334
|
+
}
|
|
335
|
+
function writeT(o, p, v) {
|
|
336
|
+
p.t = v;
|
|
337
|
+
o.public.parts[p.spec.id] = v;
|
|
338
|
+
}
|
|
339
|
+
function gated(o, p) {
|
|
340
|
+
return !!p.spec.requires && o.parts.has(p.spec.requires) && !isOpen(o, p.spec.requires);
|
|
341
|
+
}
|
|
342
|
+
function setOnePart(o, p, target2, instant) {
|
|
343
|
+
const next = clamp(target2, 0, p.maxT);
|
|
344
|
+
const changed = next !== p.target;
|
|
345
|
+
p.target = next;
|
|
346
|
+
if (instant) writeT(o, p, next);
|
|
347
|
+
if (changed || instant) emit({ type: "part", object: o.id, part: p.spec.id, t: p.t, target: p.target });
|
|
348
|
+
}
|
|
349
|
+
function setPartTarget(o, partId, target2, instant = false) {
|
|
350
|
+
const p = o.parts.get(partId);
|
|
351
|
+
if (!p) return;
|
|
352
|
+
const group = p.spec.group;
|
|
353
|
+
const members = group ? [...o.parts.values()].filter((q) => q.spec.group === group) : [p];
|
|
354
|
+
for (const q of members) {
|
|
355
|
+
setOne(o, q, target2, instant);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function setOne(o, p, target2, instant) {
|
|
359
|
+
setOnePart(o, p, target2, instant);
|
|
360
|
+
if (!targetIsOpen(p)) {
|
|
361
|
+
for (const q of o.parts.values()) {
|
|
362
|
+
if (q.spec.requires === p.spec.id && q.target > 0) setPartTarget(o, q.spec.id, 0, instant);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function partFrame(o, partId) {
|
|
367
|
+
if (!partId) return null;
|
|
368
|
+
const p = o.parts.get(partId);
|
|
369
|
+
if (!p) return null;
|
|
370
|
+
const m = p.spec.motion;
|
|
371
|
+
const axis = rotY(m.axis ?? [0, 1, 0], p.spec.ry ?? 0);
|
|
372
|
+
const frame = { pivot: p.spec.pivot, axis, angle: 0, offset: [0, 0, 0] };
|
|
373
|
+
if (m.kind === "slide") {
|
|
374
|
+
const d = (m.distance ?? 0.4) * p.t;
|
|
375
|
+
frame.offset = [axis[0] * d, axis[1] * d, axis[2] * d];
|
|
376
|
+
} else if (m.kind !== "scale") {
|
|
377
|
+
frame.angle = (m.angle ?? Math.PI / 2) * p.t;
|
|
378
|
+
}
|
|
379
|
+
return frame;
|
|
380
|
+
}
|
|
381
|
+
function applyPart(o, partId, local) {
|
|
382
|
+
const f = partFrame(o, partId);
|
|
383
|
+
if (!f) return local;
|
|
384
|
+
const rel = [local[0] - f.pivot[0], local[1] - f.pivot[1], local[2] - f.pivot[2]];
|
|
385
|
+
const r = f.angle !== 0 ? rotateAxis(rel, f.axis, f.angle) : rel;
|
|
386
|
+
return [f.pivot[0] + r[0] + f.offset[0], f.pivot[1] + r[1] + f.offset[1], f.pivot[2] + r[2] + f.offset[2]];
|
|
387
|
+
}
|
|
388
|
+
function fitBounds(o, p, b, blockedAt) {
|
|
389
|
+
const STEP = 0.05;
|
|
390
|
+
const ANGLES = 48;
|
|
391
|
+
const m = p.spec.motion;
|
|
392
|
+
const axis = rotY(m.axis ?? [0, 1, 0], p.spec.ry ?? 0);
|
|
393
|
+
const lo = [Math.min(b[0], b[3]), Math.min(b[1], b[4]), Math.min(b[2], b[5])];
|
|
394
|
+
const hi = [Math.max(b[0], b[3]), Math.max(b[1], b[4]), Math.max(b[2], b[5])];
|
|
395
|
+
const axisPoints = (i) => {
|
|
396
|
+
const n = Math.max(1, Math.ceil((hi[i] - lo[i]) / STEP));
|
|
397
|
+
return Array.from({ length: n + 1 }, (_, k) => lo[i] + (hi[i] - lo[i]) * k / n);
|
|
398
|
+
};
|
|
399
|
+
const xs = axisPoints(0);
|
|
400
|
+
const ys = axisPoints(1);
|
|
401
|
+
const zs = axisPoints(2);
|
|
402
|
+
const flat = m.kind === "slide" || Math.abs(axis[1]) > 0.999;
|
|
403
|
+
const seen = /* @__PURE__ */ new Set();
|
|
404
|
+
const pts = [];
|
|
405
|
+
const add2 = (x, y, z) => {
|
|
406
|
+
const key = `${x.toFixed(5)},${y.toFixed(5)},${z.toFixed(5)}`;
|
|
407
|
+
if (seen.has(key)) return;
|
|
408
|
+
seen.add(key);
|
|
409
|
+
pts.push([x, y, z]);
|
|
410
|
+
};
|
|
411
|
+
if (flat) {
|
|
412
|
+
for (const x of xs) for (const z of [lo[2], hi[2]]) add2(x, lo[1], z);
|
|
413
|
+
for (const z of zs) for (const x of [lo[0], hi[0]]) add2(x, lo[1], z);
|
|
414
|
+
} else {
|
|
415
|
+
for (const x of xs) for (const y of [lo[1], hi[1]]) for (const z of [lo[2], hi[2]]) add2(x, y, z);
|
|
416
|
+
for (const y of ys) for (const x of [lo[0], hi[0]]) for (const z of [lo[2], hi[2]]) add2(x, y, z);
|
|
417
|
+
for (const z of zs) for (const x of [lo[0], hi[0]]) for (const y of [lo[1], hi[1]]) add2(x, y, z);
|
|
418
|
+
}
|
|
419
|
+
const tr = o.public.transform;
|
|
420
|
+
const pivot = p.spec.pivot;
|
|
421
|
+
for (let i = 1; i <= ANGLES; i++) {
|
|
422
|
+
const tt = i / ANGLES;
|
|
423
|
+
const angle = m.kind === "slide" ? 0 : (m.angle ?? Math.PI / 2) * tt;
|
|
424
|
+
const d = m.kind === "slide" ? (m.distance ?? 0.4) * tt : 0;
|
|
425
|
+
for (const [x, y, z] of pts) {
|
|
426
|
+
let q = [x, y, z];
|
|
427
|
+
if (angle !== 0) {
|
|
428
|
+
const r = rotateAxis([x - pivot[0], y - pivot[1], z - pivot[2]], axis, angle);
|
|
429
|
+
q = [pivot[0] + r[0], pivot[1] + r[1], pivot[2] + r[2]];
|
|
430
|
+
}
|
|
431
|
+
const w = localToWorld(tr, [q[0] + axis[0] * d, 0, q[2] + axis[2] * d]);
|
|
432
|
+
if (blockedAt(w[0], w[2])) return (i - 1) / ANGLES;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
function fitSweep(objectId, partId, blocked) {
|
|
438
|
+
const o = objectOrThrow(objectId);
|
|
439
|
+
const p = o.parts.get(partId);
|
|
440
|
+
if (!p) throw new Error(`Unknown part "${partId}" on "${objectId}"`);
|
|
441
|
+
const sweep = p.spec.sweep;
|
|
442
|
+
let result = 1;
|
|
443
|
+
const bounds = p.spec.bounds;
|
|
444
|
+
const mk = p.spec.motion.kind;
|
|
445
|
+
if (bounds && (mk === "hinge" || mk === "rocker" || mk === "knob" || mk === "slide")) {
|
|
446
|
+
result = fitBounds(o, p, bounds, blocked);
|
|
447
|
+
} else if (sweep) {
|
|
448
|
+
const x0 = Math.min(sweep[0], sweep[2]);
|
|
449
|
+
const x1 = Math.max(sweep[0], sweep[2]);
|
|
450
|
+
const z0 = Math.min(sweep[1], sweep[3]);
|
|
451
|
+
const z1 = Math.max(sweep[1], sweep[3]);
|
|
452
|
+
const tr = o.public.transform;
|
|
453
|
+
const hit = (lx, lz) => {
|
|
454
|
+
const w = localToWorld(tr, [lx, 0, lz]);
|
|
455
|
+
return blocked(w[0], w[2]);
|
|
456
|
+
};
|
|
457
|
+
const m = p.spec.motion;
|
|
458
|
+
const axis = rotY(m.axis ?? [0, 1, 0], p.spec.ry ?? 0);
|
|
459
|
+
if (m.kind === "hinge" || m.kind === "rocker" || m.kind === "knob") {
|
|
460
|
+
if (Math.abs(axis[1]) >= 0.5) {
|
|
461
|
+
const angle = m.angle ?? Math.PI / 2;
|
|
462
|
+
const total = -angle * Math.sign(axis[1]);
|
|
463
|
+
const [px, , pz] = p.spec.pivot;
|
|
464
|
+
const cx = (x0 + x1) / 2;
|
|
465
|
+
const cz = (z0 + z1) / 2;
|
|
466
|
+
const w = x1 - x0;
|
|
467
|
+
const d = z1 - z0;
|
|
468
|
+
const radius = Math.abs(angle) > 1.7 ? Math.min(w, d) : Math.max(w, d);
|
|
469
|
+
const tol = 0.05;
|
|
470
|
+
const inRect = (x, z) => x >= x0 - tol && x <= x1 + tol && z >= z0 - tol && z <= z1 + tol;
|
|
471
|
+
const centered = Math.hypot(cx - px, cz - pz) < 1e-6;
|
|
472
|
+
const mid = Math.atan2(cz - pz, cx - px);
|
|
473
|
+
for (let i = 1; i <= SWEEP_STEPS && result === 1; i++) {
|
|
474
|
+
const tt = i / SWEEP_STEPS;
|
|
475
|
+
let isHit = false;
|
|
476
|
+
if (centered) {
|
|
477
|
+
for (let x = x0; x <= x1 + 1e-9 && !isHit; x += 0.1) for (let z = z0; z <= z1 + 1e-9 && !isHit; z += 0.1) isHit = hit(x, z);
|
|
478
|
+
} else {
|
|
479
|
+
const phi = mid - total / 2 + total * tt;
|
|
480
|
+
const steps = Math.max(2, Math.ceil(radius / 0.1));
|
|
481
|
+
for (let k = 1; k <= steps && !isHit; k++) {
|
|
482
|
+
const r = radius * k / steps;
|
|
483
|
+
const x = px + Math.cos(phi) * r;
|
|
484
|
+
const z = pz + Math.sin(phi) * r;
|
|
485
|
+
if (inRect(x, z)) isHit = hit(x, z);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (isHit) result = (i - 1) / SWEEP_STEPS;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} else if (m.kind === "slide") {
|
|
492
|
+
const len = Math.hypot(axis[0], axis[2]);
|
|
493
|
+
if (len > 1e-6) {
|
|
494
|
+
const dx = axis[0] / len;
|
|
495
|
+
const dz = axis[2] / len;
|
|
496
|
+
const proj = [x0 * dx + z0 * dz, x1 * dx + z0 * dz, x0 * dx + z1 * dz, x1 * dx + z1 * dz];
|
|
497
|
+
const pmin = Math.min(...proj);
|
|
498
|
+
const pmax = Math.max(...proj);
|
|
499
|
+
const nx = Math.max(1, Math.ceil((x1 - x0) / 0.1));
|
|
500
|
+
const nz = Math.max(1, Math.ceil((z1 - z0) / 0.1));
|
|
501
|
+
for (let i = 1; i <= SWEEP_STEPS && result === 1; i++) {
|
|
502
|
+
const limit = pmin + (pmax - pmin) * (i / SWEEP_STEPS);
|
|
503
|
+
let isHit = false;
|
|
504
|
+
for (let a = 0; a <= nx && !isHit; a++) {
|
|
505
|
+
for (let b = 0; b <= nz && !isHit; b++) {
|
|
506
|
+
const x = x0 + (x1 - x0) * a / nx;
|
|
507
|
+
const z = z0 + (z1 - z0) * b / nz;
|
|
508
|
+
if (x * dx + z * dz <= limit + 1e-9 && hit(x, z)) isHit = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (isHit) result = (i - 1) / SWEEP_STEPS;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
p.maxT = result;
|
|
517
|
+
if (p.target > result) p.target = result;
|
|
518
|
+
if (p.t > result) writeT(o, p, result);
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
function containerOf(o, id) {
|
|
522
|
+
return o.spec.containers?.find((c) => c.id === id);
|
|
523
|
+
}
|
|
524
|
+
function accessible(objectId, containerId) {
|
|
525
|
+
const o = objs.get(objectId);
|
|
526
|
+
if (!o) return false;
|
|
527
|
+
const c = containerOf(o, containerId);
|
|
528
|
+
if (!c) return false;
|
|
529
|
+
return isOpen(o, c.access);
|
|
530
|
+
}
|
|
531
|
+
function contents(objectId, containerId) {
|
|
532
|
+
return (objs.get(objectId)?.contents.get(containerId) ?? []).map(cloneStack);
|
|
533
|
+
}
|
|
534
|
+
function take(objectId, containerId, slot, actor = DEFAULT_ACTOR) {
|
|
535
|
+
const o = objs.get(objectId);
|
|
536
|
+
if (!o) return null;
|
|
537
|
+
const list = o.contents.get(containerId);
|
|
538
|
+
if (!list || !accessible(objectId, containerId)) return null;
|
|
539
|
+
const stack = list[slot];
|
|
540
|
+
if (!stack) return null;
|
|
541
|
+
const def = options.items?.[stack.type];
|
|
542
|
+
if (def && def.pickable === false) return null;
|
|
543
|
+
list[slot] = null;
|
|
544
|
+
emit({ type: "pickup", object: objectId, container: containerId, slot, stack: cloneStack(stack), actor });
|
|
545
|
+
return stack;
|
|
546
|
+
}
|
|
547
|
+
function put(objectId, containerId, stack) {
|
|
548
|
+
const o = objs.get(objectId);
|
|
549
|
+
if (!o) return null;
|
|
550
|
+
const c = containerOf(o, containerId);
|
|
551
|
+
const list = o.contents.get(containerId);
|
|
552
|
+
if (!c || !list || !accessible(objectId, containerId)) return null;
|
|
553
|
+
const maxStack = options.items?.[stack.type]?.stack ?? 1;
|
|
554
|
+
for (let i = 0; i < list.length; i++) {
|
|
555
|
+
const cur = list[i];
|
|
556
|
+
if (cur && cur.type === stack.type && !cur.data && !stack.data && cur.qty + stack.qty <= maxStack) {
|
|
557
|
+
cur.qty += stack.qty;
|
|
558
|
+
return i;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (let i = 0; i < list.length; i++) {
|
|
562
|
+
if (!list[i] && fitsSlot(stack.type, c.slots[i], options.items)) {
|
|
563
|
+
list[i] = cloneStack(stack);
|
|
564
|
+
return i;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
message("full");
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
function slotPosition(objectId, containerId, slot) {
|
|
571
|
+
const o = objectOrThrow(objectId);
|
|
572
|
+
const c = containerOf(o, containerId);
|
|
573
|
+
const s = c?.slots[slot];
|
|
574
|
+
if (!c || !s) throw new Error(`Unknown slot ${slot} of "${containerId}" on "${objectId}"`);
|
|
575
|
+
return toWorld(o, applyPart(o, s.part, s.pos));
|
|
576
|
+
}
|
|
577
|
+
function rebuildPlugs() {
|
|
578
|
+
plugs.clear();
|
|
579
|
+
const sockets = [];
|
|
580
|
+
for (const o of objs.values()) {
|
|
581
|
+
for (const s of o.spec.sockets ?? []) sockets.push({ id: gid(o, s.id), local: s.id, pos: toWorld(o, s.pos), circuit: s.circuit });
|
|
582
|
+
}
|
|
583
|
+
const used = /* @__PURE__ */ new Set();
|
|
584
|
+
const autos = [];
|
|
585
|
+
for (const o of objs.values()) {
|
|
586
|
+
for (const d of o.devices.values()) {
|
|
587
|
+
const key = gid(o, d.spec.id);
|
|
588
|
+
const want = d.spec.socket;
|
|
589
|
+
if (want === void 0 || d.spec.circuit) {
|
|
590
|
+
plugs.set(key, null);
|
|
591
|
+
} else if (want === "auto") {
|
|
592
|
+
plugs.set(key, null);
|
|
593
|
+
autos.push({ o, d });
|
|
594
|
+
} else {
|
|
595
|
+
const s = sockets.find((x) => x.id === want) ?? sockets.find((x) => x.id === gid(o, want)) ?? sockets.find((x) => x.local === want);
|
|
596
|
+
plugs.set(key, s ? s.id : null);
|
|
597
|
+
if (s) used.add(s.id);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const { o, d } of autos) {
|
|
602
|
+
const from = d.spec.plug ? toWorld(o, d.spec.plug) : originOf(o);
|
|
603
|
+
let best = null;
|
|
604
|
+
let bestD = AUTO_RANGE;
|
|
605
|
+
for (const s of sockets) {
|
|
606
|
+
if (used.has(s.id)) continue;
|
|
607
|
+
const dd = dist3(from, s.pos);
|
|
608
|
+
if (dd <= bestD) {
|
|
609
|
+
best = s;
|
|
610
|
+
bestD = dd;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (best) {
|
|
614
|
+
used.add(best.id);
|
|
615
|
+
plugs.set(gid(o, d.spec.id), best.id);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function breaker(circuit) {
|
|
620
|
+
return breakers.get(circuit) ?? true;
|
|
621
|
+
}
|
|
622
|
+
function devicePowered(o, d) {
|
|
623
|
+
if (d.spec.electric === false) return true;
|
|
624
|
+
if (!grid) return false;
|
|
625
|
+
if (d.spec.circuit) return breaker(d.spec.circuit);
|
|
626
|
+
if (d.spec.socket === void 0) return false;
|
|
627
|
+
const sid = plugs.get(gid(o, d.spec.id));
|
|
628
|
+
if (!sid) return false;
|
|
629
|
+
const found = findGlobal(sid, (oo, local) => oo.spec.sockets?.find((s) => s.id === local));
|
|
630
|
+
if (!found) return false;
|
|
631
|
+
return found.value.circuit ? breaker(found.value.circuit) : true;
|
|
632
|
+
}
|
|
633
|
+
function getDevice(objectId, deviceId) {
|
|
634
|
+
const o = objs.get(objectId);
|
|
635
|
+
const d = o?.devices.get(deviceId);
|
|
636
|
+
return o && d ? { o, d } : null;
|
|
637
|
+
}
|
|
638
|
+
function powerSnapshot() {
|
|
639
|
+
const m = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const o of objs.values()) for (const d of o.devices.values()) m.set(gid(o, d.spec.id), d.on && devicePowered(o, d));
|
|
641
|
+
return m;
|
|
642
|
+
}
|
|
643
|
+
function emitPowerChanges(before) {
|
|
644
|
+
for (const o of objs.values()) {
|
|
645
|
+
for (const d of o.devices.values()) {
|
|
646
|
+
const key = gid(o, d.spec.id);
|
|
647
|
+
const now = d.on && devicePowered(o, d);
|
|
648
|
+
if (before.get(key) !== now && d.on) emit({ type: "device", object: o.id, device: d.spec.id, on: d.on, powered: devicePowered(o, d), channel: d.spec.kind === "screen" ? d.channel : void 0 });
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function setDeviceOn(o, d, on) {
|
|
653
|
+
d.on = on;
|
|
654
|
+
if (d.spec.part) setPartTarget(o, d.spec.part, on ? 1 : 0);
|
|
655
|
+
const powered = devicePowered(o, d);
|
|
656
|
+
emit({ type: "device", object: o.id, device: d.spec.id, on, powered, channel: d.spec.kind === "screen" ? d.channel : void 0 });
|
|
657
|
+
if (on && !powered) message("no_power");
|
|
658
|
+
}
|
|
659
|
+
function setDevice(objectId, deviceId, on) {
|
|
660
|
+
const f = getDevice(objectId, deviceId);
|
|
661
|
+
if (!f) return false;
|
|
662
|
+
if (f.d.on !== on) setDeviceOn(f.o, f.d, on);
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
function setFixture(objectId, fixtureId, on) {
|
|
666
|
+
const fx = objs.get(objectId)?.fixtures.get(fixtureId);
|
|
667
|
+
if (!fx) return false;
|
|
668
|
+
if (fx.on !== on) toggleFixture(objs.get(objectId), fixtureId);
|
|
669
|
+
return fx.on === on;
|
|
670
|
+
}
|
|
671
|
+
function setBurner(objectId, burnerId, on) {
|
|
672
|
+
const o = objs.get(objectId);
|
|
673
|
+
const b = o?.burners.get(burnerId);
|
|
674
|
+
if (!o || !b) return false;
|
|
675
|
+
const isOn = b.on || b.lit;
|
|
676
|
+
if (on === isOn) return true;
|
|
677
|
+
if (!on) return toggleBurner(o, burnerId);
|
|
678
|
+
return b.spec.needsIgnition ? ignite(o, burnerId) : toggleBurner(o, burnerId);
|
|
679
|
+
}
|
|
680
|
+
function resolveDevice(o, id) {
|
|
681
|
+
if (id.includes(":")) {
|
|
682
|
+
const f = findGlobal(id, (oo, local2) => oo.devices.get(local2));
|
|
683
|
+
return f ? { o: f.o, d: f.value } : null;
|
|
684
|
+
}
|
|
685
|
+
const local = o.devices.get(id);
|
|
686
|
+
if (local) return { o, d: local };
|
|
687
|
+
for (const oo of objs.values()) {
|
|
688
|
+
const d = oo.devices.get(id);
|
|
689
|
+
if (d) return { o: oo, d };
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
function switchIsOn(o, spec) {
|
|
694
|
+
if (spec.part && o.parts.has(spec.part)) return targetIsOpen(o.parts.get(spec.part));
|
|
695
|
+
return spec.controls.some((id) => resolveDevice(o, id)?.d.on);
|
|
696
|
+
}
|
|
697
|
+
function toggleSwitch(o, id) {
|
|
698
|
+
const sw = o.spec.switches?.find((s) => s.id === id);
|
|
699
|
+
if (!sw) return false;
|
|
700
|
+
const on = !switchIsOn(o, sw);
|
|
701
|
+
if (sw.part) setPartTarget(o, sw.part, on ? 1 : 0);
|
|
702
|
+
let unpowered = false;
|
|
703
|
+
for (const ref of sw.controls) {
|
|
704
|
+
const r = resolveDevice(o, ref);
|
|
705
|
+
if (!r) continue;
|
|
706
|
+
r.d.on = on;
|
|
707
|
+
if (r.d.spec.part) setPartTarget(r.o, r.d.spec.part, on ? 1 : 0);
|
|
708
|
+
const powered = devicePowered(r.o, r.d);
|
|
709
|
+
if (on && !powered) unpowered = true;
|
|
710
|
+
emit({ type: "device", object: r.o.id, device: r.d.spec.id, on, powered });
|
|
711
|
+
}
|
|
712
|
+
emit({ type: "switch", object: o.id, switch: sw.id, on });
|
|
713
|
+
if (unpowered) message("no_power");
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
function resolveFuel(o, b) {
|
|
717
|
+
const want = b.spec.fuel;
|
|
718
|
+
if (want === "auto") {
|
|
719
|
+
const from = toWorld(o, b.spec.pos);
|
|
720
|
+
let best = null;
|
|
721
|
+
let bestD = AUTO_RANGE;
|
|
722
|
+
for (const oo of objs.values()) {
|
|
723
|
+
for (const f of oo.fuels.values()) {
|
|
724
|
+
if (f.spec.kind !== "gas") continue;
|
|
725
|
+
const d = dist3(from, originOf(oo));
|
|
726
|
+
if (d <= bestD) {
|
|
727
|
+
best = { o: oo, f };
|
|
728
|
+
bestD = d;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return best;
|
|
733
|
+
}
|
|
734
|
+
if (want.includes(":")) {
|
|
735
|
+
const f = findGlobal(want, (oo, local2) => oo.fuels.get(local2));
|
|
736
|
+
return f ? { o: f.o, f: f.value } : null;
|
|
737
|
+
}
|
|
738
|
+
const local = o.fuels.get(want);
|
|
739
|
+
return local ? { o, f: local } : null;
|
|
740
|
+
}
|
|
741
|
+
function valveOpen(o, f) {
|
|
742
|
+
const v = f.spec.valve;
|
|
743
|
+
if (!v || !o.parts.has(v)) return true;
|
|
744
|
+
return targetIsOpen(o.parts.get(v));
|
|
745
|
+
}
|
|
746
|
+
function canBurn(b, ref) {
|
|
747
|
+
return !!ref && ref.f.level > 0 && valveOpen(ref.o, ref.f);
|
|
748
|
+
}
|
|
749
|
+
function isSolid(ref) {
|
|
750
|
+
return !!ref && ref.f.spec.kind !== "gas";
|
|
751
|
+
}
|
|
752
|
+
function emitBurner(o, b) {
|
|
753
|
+
emit({ type: "burner", object: o.id, burner: b.spec.id, on: b.on, lit: b.lit });
|
|
754
|
+
}
|
|
755
|
+
function extinguish(o, b, turnOff) {
|
|
756
|
+
const ref = resolveFuel(o, b);
|
|
757
|
+
const wasLit = b.lit;
|
|
758
|
+
b.lit = false;
|
|
759
|
+
if (turnOff) b.on = false;
|
|
760
|
+
if (b.spec.part) setPartTarget(o, b.spec.part, b.on ? 1 : 0);
|
|
761
|
+
if (wasLit && isSolid(ref)) b.smoke = SMOKE_TAIL;
|
|
762
|
+
emitBurner(o, b);
|
|
763
|
+
}
|
|
764
|
+
function toggleBurner(o, id) {
|
|
765
|
+
const b = o.burners.get(id);
|
|
766
|
+
if (!b) return false;
|
|
767
|
+
if (b.on || b.lit) {
|
|
768
|
+
extinguish(o, b, true);
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
if (b.spec.needsIgnition) return false;
|
|
772
|
+
const ref = resolveFuel(o, b);
|
|
773
|
+
b.on = true;
|
|
774
|
+
b.lit = canBurn(b, ref);
|
|
775
|
+
if (b.lit) b.smoke = 0;
|
|
776
|
+
if (b.spec.part) setPartTarget(o, b.spec.part, 1);
|
|
777
|
+
emitBurner(o, b);
|
|
778
|
+
if (!b.lit) message(ref && ref.f.level <= 0 ? "empty" : "no_gas");
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
function ignite(o, id) {
|
|
782
|
+
const b = o.burners.get(id);
|
|
783
|
+
if (!b || b.lit) return false;
|
|
784
|
+
const ref = resolveFuel(o, b);
|
|
785
|
+
if (!canBurn(b, ref)) {
|
|
786
|
+
message(ref && ref.f.level <= 0 ? "empty" : "no_gas");
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
b.on = true;
|
|
790
|
+
b.lit = true;
|
|
791
|
+
b.smoke = 0;
|
|
792
|
+
if (b.spec.part) setPartTarget(o, b.spec.part, 1);
|
|
793
|
+
emitBurner(o, b);
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
function toggleFuel(o, id) {
|
|
797
|
+
const f = o.fuels.get(id);
|
|
798
|
+
if (!f || !f.spec.valve || !o.parts.has(f.spec.valve)) return false;
|
|
799
|
+
const p = o.parts.get(f.spec.valve);
|
|
800
|
+
setPartTarget(o, f.spec.valve, targetIsOpen(p) ? 0 : 1);
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
function resolveTank(o, id) {
|
|
804
|
+
const want = id === void 0 || id === "auto" ? defaultTank : id;
|
|
805
|
+
if (!want) return null;
|
|
806
|
+
const f = findGlobal(want, (oo, local2) => oo.tanks.get(local2));
|
|
807
|
+
if (f) return { o: f.o, t: f.value };
|
|
808
|
+
const local = o.tanks.get(want);
|
|
809
|
+
return local ? { o, t: local } : null;
|
|
810
|
+
}
|
|
811
|
+
function drawWater(o, fx, liters) {
|
|
812
|
+
if (liters <= 0) return 0;
|
|
813
|
+
if (options.waterSupply) return Math.max(0, Math.min(liters, options.waterSupply.draw(gid(o, fx.spec.id), liters)));
|
|
814
|
+
const tank = resolveTank(o, fx.spec.tank);
|
|
815
|
+
if (!tank) return 0;
|
|
816
|
+
const got = Math.min(liters, tank.t.level);
|
|
817
|
+
tank.t.level -= got;
|
|
818
|
+
return got;
|
|
819
|
+
}
|
|
820
|
+
function emitFixture(o, fx) {
|
|
821
|
+
emit({ type: "fixture", object: o.id, fixture: fx.spec.id, on: fx.on, flowing: fx.flowing });
|
|
822
|
+
}
|
|
823
|
+
function flowOf(fx) {
|
|
824
|
+
return fx.spec.flow ?? FLOW_DEFAULTS[fx.spec.kind] ?? 6;
|
|
825
|
+
}
|
|
826
|
+
function stepFixture(o, fx, dt) {
|
|
827
|
+
if (!fx.on) return;
|
|
828
|
+
if (fx.spec.kind === "flush") {
|
|
829
|
+
fx.timer -= dt;
|
|
830
|
+
if (fx.timer <= 0) {
|
|
831
|
+
fx.on = false;
|
|
832
|
+
fx.flowing = false;
|
|
833
|
+
fx.frac = 0;
|
|
834
|
+
if (fx.spec.part) setPartTarget(o, fx.spec.part, 0);
|
|
835
|
+
emitFixture(o, fx);
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (dt <= 0) return;
|
|
840
|
+
const wanted = flowOf(fx) / 60 * dt;
|
|
841
|
+
const got = drawWater(o, fx, wanted);
|
|
842
|
+
const flowing = got > 1e-9;
|
|
843
|
+
fx.frac = wanted > 0 ? Math.min(1, got / wanted) : 0;
|
|
844
|
+
if (flowing !== fx.flowing) {
|
|
845
|
+
fx.flowing = flowing;
|
|
846
|
+
emitFixture(o, fx);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function toggleFixture(o, id) {
|
|
850
|
+
const fx = o.fixtures.get(id);
|
|
851
|
+
if (!fx) return false;
|
|
852
|
+
if (fx.on) {
|
|
853
|
+
fx.on = false;
|
|
854
|
+
fx.flowing = false;
|
|
855
|
+
fx.frac = 0;
|
|
856
|
+
if (fx.spec.part) setPartTarget(o, fx.spec.part, 0);
|
|
857
|
+
emitFixture(o, fx);
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
fx.on = true;
|
|
861
|
+
if (fx.spec.part) setPartTarget(o, fx.spec.part, 1);
|
|
862
|
+
if (fx.spec.kind === "flush") {
|
|
863
|
+
const volume2 = fx.spec.volume ?? 6;
|
|
864
|
+
const got = drawWater(o, fx, volume2);
|
|
865
|
+
fx.flowing = got > 1e-9;
|
|
866
|
+
fx.frac = fx.flowing ? 1 : 0;
|
|
867
|
+
fx.timer = FLUSH_SECONDS;
|
|
868
|
+
if (!fx.flowing) {
|
|
869
|
+
fx.on = false;
|
|
870
|
+
if (fx.spec.part) setPartTarget(o, fx.spec.part, 0);
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
fx.flowing = false;
|
|
874
|
+
fx.frac = 0;
|
|
875
|
+
stepFixture(o, fx, 1 / 60);
|
|
876
|
+
}
|
|
877
|
+
emitFixture(o, fx);
|
|
878
|
+
if (!fx.flowing) message("no_water");
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
function seatTaken(objectId, seatId) {
|
|
882
|
+
for (const [actor, s] of seats) if (s.object === objectId && s.seat === seatId) return actor;
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
function stand(actor = DEFAULT_ACTOR) {
|
|
886
|
+
const s = seats.get(actor);
|
|
887
|
+
if (!s) return null;
|
|
888
|
+
seats.delete(actor);
|
|
889
|
+
const o = objs.get(s.object);
|
|
890
|
+
const spec = o?.spec.seats?.find((x) => x.id === s.seat);
|
|
891
|
+
emit({ type: "seat", object: s.object, seat: s.seat, actor, seated: false });
|
|
892
|
+
if (!o || !spec) return null;
|
|
893
|
+
return toWorld(o, spec.exit);
|
|
894
|
+
}
|
|
895
|
+
function sit(objectId, seatId, actor = DEFAULT_ACTOR) {
|
|
896
|
+
const o = objs.get(objectId);
|
|
897
|
+
const seat = o?.spec.seats?.find((s) => s.id === seatId);
|
|
898
|
+
if (!o || !seat) return null;
|
|
899
|
+
const holder = seatTaken(objectId, seatId);
|
|
900
|
+
if (holder && holder !== actor) return null;
|
|
901
|
+
if (seats.has(actor) && !holder) stand(actor);
|
|
902
|
+
seats.set(actor, { object: objectId, seat: seatId });
|
|
903
|
+
const eye = seat.eye ?? (seat.pose === "lie" ? 0.18 : 0.62);
|
|
904
|
+
emit({ type: "seat", object: objectId, seat: seatId, actor, seated: true });
|
|
905
|
+
return { eye: toWorld(o, [seat.pos[0], seat.pos[1] + eye, seat.pos[2]]), yaw: o.public.transform.ry + seat.ry + Math.PI, pose: seat.pose };
|
|
906
|
+
}
|
|
907
|
+
function push(pos, radius, vel) {
|
|
908
|
+
for (const o of objs.values()) {
|
|
909
|
+
if (!o.soft.size) continue;
|
|
910
|
+
const tr = o.public.transform;
|
|
911
|
+
const lp = worldToLocal(tr, pos);
|
|
912
|
+
const lv = rotY(vel, -tr.ry);
|
|
913
|
+
for (const s of o.soft.values()) {
|
|
914
|
+
const [w, h, d] = s.spec.size;
|
|
915
|
+
const hx = w / 2;
|
|
916
|
+
const hz = Math.max(d, 0.1) / 2;
|
|
917
|
+
const dx = Math.max(Math.abs(lp[0] - s.spec.anchor[0]) - hx, 0);
|
|
918
|
+
const dz = Math.max(Math.abs(lp[2] - s.spec.anchor[2]) - hz, 0);
|
|
919
|
+
if (Math.hypot(dx, dz) > radius) continue;
|
|
920
|
+
const top = tr.y + s.spec.anchor[1];
|
|
921
|
+
if (pos[1] + ACTOR_HEIGHT < top - h || pos[1] > top) continue;
|
|
922
|
+
const cap = 8;
|
|
923
|
+
s.vax = clamp(s.vax - lv[2] * PUSH_GAIN, -cap, cap);
|
|
924
|
+
s.vaz = clamp(s.vaz + lv[0] * PUSH_GAIN, -cap, cap);
|
|
925
|
+
if (s.spec.kind === "panel" && s.spec.part && lv[2] < -0.05 && lp[2] > s.spec.anchor[2]) setPartTarget(o, s.spec.part, 1);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function stepSoft(dt) {
|
|
930
|
+
const sub = Math.max(1, Math.ceil(dt / (1 / 120)));
|
|
931
|
+
const h = dt / sub;
|
|
932
|
+
for (const o of objs.values()) {
|
|
933
|
+
if (!o.soft.size) continue;
|
|
934
|
+
const wl = rotY([wind[0], 0, wind[1]], -o.public.transform.ry);
|
|
935
|
+
for (const s of o.soft.values()) {
|
|
936
|
+
const k = s.spec.stiffness ?? 30;
|
|
937
|
+
const c = s.spec.damping ?? 4;
|
|
938
|
+
const max = s.spec.maxAngle ?? 0.7;
|
|
939
|
+
const gain = s.spec.wind ?? (s.spec.kind === "panel" ? 0 : 1);
|
|
940
|
+
const gust = gain * (0.6 + 0.4 * Math.sin(time * 1.7 + s.phase)) * 0.1;
|
|
941
|
+
const fx = -wl[2] * gust;
|
|
942
|
+
const fz = wl[0] * gust;
|
|
943
|
+
for (let i = 0; i < sub; i++) {
|
|
944
|
+
s.vax += (-k * s.ax - c * s.vax + fx) * h;
|
|
945
|
+
s.vaz += (-k * s.az - c * s.vaz + fz) * h;
|
|
946
|
+
s.ax += s.vax * h;
|
|
947
|
+
s.az += s.vaz * h;
|
|
948
|
+
}
|
|
949
|
+
if (Math.abs(s.ax) > max) {
|
|
950
|
+
s.ax = Math.sign(s.ax) * max;
|
|
951
|
+
s.vax = 0;
|
|
952
|
+
}
|
|
953
|
+
if (Math.abs(s.az) > max) {
|
|
954
|
+
s.az = Math.sign(s.az) * max;
|
|
955
|
+
s.vaz = 0;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function damage(objectId, target2, amount, _kind) {
|
|
961
|
+
const o = objs.get(objectId);
|
|
962
|
+
if (o && options.destruction) {
|
|
963
|
+
const pos2 = toWorld(o, [0, 0.5, 0]);
|
|
964
|
+
const type = _kind === "gas" ? "explosive" : _kind === "fabric" ? "slash" : "blunt";
|
|
965
|
+
const result = options.destruction.hitPart(objectId, target2, { type, point: pos2, energy: Math.max(0, amount), source: "interactables" });
|
|
966
|
+
const destroyed2 = !!result?.events?.some((e) => e.type === "fracture" && e.part === target2);
|
|
967
|
+
emit({ type: "damage", object: objectId, target: target2, hp: -1, destroyed: destroyed2 });
|
|
968
|
+
return { hp: -1, destroyed: destroyed2 };
|
|
969
|
+
}
|
|
970
|
+
const st = o?.hp.get(target2);
|
|
971
|
+
if (!o || !st) return null;
|
|
972
|
+
if (st.hp <= 0) return { hp: 0, destroyed: true };
|
|
973
|
+
const before = st.hp;
|
|
974
|
+
st.hp = Math.max(0, st.hp - Math.max(0, amount));
|
|
975
|
+
const destroyed = st.hp <= 0;
|
|
976
|
+
emit({ type: "damage", object: objectId, target: target2, hp: st.hp, destroyed });
|
|
977
|
+
const pos = toWorld(o, [0, 0.5, 0]);
|
|
978
|
+
const fuel = o.fuels.get(target2);
|
|
979
|
+
if (fuel && destroyed && fuel.spec.kind === "gas" && fuel.level > 0) {
|
|
980
|
+
emit({ type: "explosion", object: objectId, pos, radius: 2 + fuel.level / 5 });
|
|
981
|
+
fuel.level = 0;
|
|
982
|
+
}
|
|
983
|
+
const tank = o.tanks.get(target2);
|
|
984
|
+
if (tank && before === st.max && st.hp < before) emit({ type: "leak", object: objectId, target: target2, pos });
|
|
985
|
+
return { hp: st.hp, destroyed };
|
|
986
|
+
}
|
|
987
|
+
function deriveInteractions(spec) {
|
|
988
|
+
const out = [];
|
|
989
|
+
const used = /* @__PURE__ */ new Set();
|
|
990
|
+
const partPos = (id, fallback) => spec.parts?.find((p) => p.id === id)?.pivot ?? fallback;
|
|
991
|
+
const partLabel = (id) => spec.parts?.find((p) => p.id === id)?.label ?? "";
|
|
992
|
+
const claim = (id) => {
|
|
993
|
+
if (id) used.add(id);
|
|
994
|
+
};
|
|
995
|
+
for (const d of spec.devices ?? []) {
|
|
996
|
+
claim(d.part);
|
|
997
|
+
const pos = d.part ? partPos(d.part, [0, 1, 0]) : d.plug ?? [0, 1, 0];
|
|
998
|
+
if (d.kind === "lamp" && !d.part && !d.plug && d.electric !== false) continue;
|
|
999
|
+
out.push({ id: `device.${d.id}`, pos, action: { do: "toggle_device", device: d.id }, label: partLabel(d.part) });
|
|
1000
|
+
if (d.kind === "screen" && (d.channels ?? 1) > 1) out.push({ id: `channel.${d.id}`, pos, action: { do: "cycle_device", device: d.id }, label: "" });
|
|
1001
|
+
}
|
|
1002
|
+
for (const s of spec.switches ?? []) {
|
|
1003
|
+
claim(s.part);
|
|
1004
|
+
out.push({ id: `switch.${s.id}`, pos: partPos(s.part, [0, 1.2, 0]), action: { do: "toggle_switch", switch: s.id }, label: partLabel(s.part) });
|
|
1005
|
+
}
|
|
1006
|
+
const driven = new Set((spec.devices ?? []).map((d) => d.fixture));
|
|
1007
|
+
for (const f of spec.fixtures ?? []) {
|
|
1008
|
+
claim(f.part);
|
|
1009
|
+
if (driven.has(f.id)) continue;
|
|
1010
|
+
out.push({ id: `fixture.${f.id}`, pos: f.part ? partPos(f.part, f.outlet) : f.outlet, action: { do: "toggle_fixture", fixture: f.id }, label: partLabel(f.part) });
|
|
1011
|
+
}
|
|
1012
|
+
for (const b of spec.burners ?? []) {
|
|
1013
|
+
claim(b.part);
|
|
1014
|
+
const pos = b.part ? partPos(b.part, b.pos) : b.pos;
|
|
1015
|
+
if (b.needsIgnition) out.push({ id: `ignite.${b.id}`, pos, action: { do: "ignite", burner: b.id }, label: "" });
|
|
1016
|
+
out.push({ id: `burner.${b.id}`, pos, action: { do: "toggle_burner", burner: b.id }, label: partLabel(b.part) });
|
|
1017
|
+
}
|
|
1018
|
+
for (const f of spec.fuels ?? []) {
|
|
1019
|
+
if (!f.valve) continue;
|
|
1020
|
+
claim(f.valve);
|
|
1021
|
+
out.push({ id: `fuel.${f.id}`, pos: partPos(f.valve, [0, 0.5, 0]), action: { do: "toggle_fuel", fuel: f.id }, label: partLabel(f.valve) });
|
|
1022
|
+
}
|
|
1023
|
+
for (const c of spec.containers ?? []) {
|
|
1024
|
+
if (!c.access) out.push({ id: `open.${c.id}`, pos: c.volume.center, action: { do: "open_container", container: c.id }, label: "" });
|
|
1025
|
+
}
|
|
1026
|
+
for (const p of spec.parts ?? []) {
|
|
1027
|
+
if (used.has(p.id)) continue;
|
|
1028
|
+
if (p.label) out.push({ id: `part.${p.id}`, pos: p.pivot, action: { do: "toggle_part", part: p.id }, label: p.label });
|
|
1029
|
+
}
|
|
1030
|
+
for (const c of spec.containers ?? []) {
|
|
1031
|
+
if (c.access && !spec.parts?.find((p) => p.id === c.access)?.label) {
|
|
1032
|
+
out.push({ id: `open.${c.id}`, pos: partPos(c.access, c.volume.center), action: { do: "open_container", container: c.id }, label: "" });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
for (const s of spec.seats ?? []) out.push({ id: `seat.${s.id}`, pos: s.pos, action: { do: "sit", seat: s.id }, label: "" });
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
function typeName(o) {
|
|
1039
|
+
return table[`type.${o.spec.type}`] ?? humanize(o.spec.type);
|
|
1040
|
+
}
|
|
1041
|
+
function promptOf(o, i) {
|
|
1042
|
+
const a = i.action;
|
|
1043
|
+
let state = null;
|
|
1044
|
+
let def = "";
|
|
1045
|
+
switch (a.do) {
|
|
1046
|
+
case "toggle_part": {
|
|
1047
|
+
const p = o.parts.get(a.part);
|
|
1048
|
+
const open = !!p && targetIsOpen(p);
|
|
1049
|
+
state = open ? "open" : "closed";
|
|
1050
|
+
def = open ? "prompt.close" : "prompt.open";
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
case "open_container": {
|
|
1054
|
+
const c = containerOf(o, a.container);
|
|
1055
|
+
const p = c?.access ? o.parts.get(c.access) : void 0;
|
|
1056
|
+
const open = p ? targetIsOpen(p) : false;
|
|
1057
|
+
state = p ? open ? "open" : "closed" : null;
|
|
1058
|
+
def = open ? "prompt.close" : "prompt.open";
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case "toggle_device": {
|
|
1062
|
+
const on = !!o.devices.get(a.device)?.on;
|
|
1063
|
+
state = on ? "on" : "off";
|
|
1064
|
+
def = on ? "prompt.turn_off" : "prompt.turn_on";
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case "toggle_switch": {
|
|
1068
|
+
const sw = o.spec.switches?.find((s) => s.id === a.switch);
|
|
1069
|
+
const on = !!sw && switchIsOn(o, sw);
|
|
1070
|
+
state = on ? "on" : "off";
|
|
1071
|
+
def = on ? "prompt.turn_off" : "prompt.turn_on";
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case "toggle_breaker": {
|
|
1075
|
+
const on = breaker(a.circuit);
|
|
1076
|
+
state = on ? "on" : "off";
|
|
1077
|
+
def = on ? "prompt.breaker_off" : "prompt.breaker_on";
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
case "toggle_fixture": {
|
|
1081
|
+
const on = !!o.fixtures.get(a.fixture)?.on;
|
|
1082
|
+
state = on ? "on" : "off";
|
|
1083
|
+
def = on ? "prompt.turn_off" : "prompt.turn_on";
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
case "toggle_burner": {
|
|
1087
|
+
const b = o.burners.get(a.burner);
|
|
1088
|
+
const on = !!b && (b.on || b.lit);
|
|
1089
|
+
state = on ? "on" : "off";
|
|
1090
|
+
def = on ? "prompt.turn_off" : "prompt.turn_on";
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
case "toggle_fuel": {
|
|
1094
|
+
const f = o.fuels.get(a.fuel);
|
|
1095
|
+
const open = !!f && valveOpen(o, f);
|
|
1096
|
+
state = open ? "open" : "closed";
|
|
1097
|
+
def = open ? "prompt.close_valve" : "prompt.open_valve";
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
case "take":
|
|
1101
|
+
def = "prompt.take";
|
|
1102
|
+
break;
|
|
1103
|
+
case "read":
|
|
1104
|
+
def = "prompt.read";
|
|
1105
|
+
break;
|
|
1106
|
+
case "ignite":
|
|
1107
|
+
def = "prompt.light";
|
|
1108
|
+
break;
|
|
1109
|
+
case "cycle_device":
|
|
1110
|
+
def = "prompt.change_channel";
|
|
1111
|
+
break;
|
|
1112
|
+
case "sit": {
|
|
1113
|
+
const seat = o.spec.seats?.find((s) => s.id === a.seat);
|
|
1114
|
+
const mine = seats.get(DEFAULT_ACTOR);
|
|
1115
|
+
def = mine && mine.object === o.id && mine.seat === a.seat ? "prompt.get_up" : seat?.pose === "lie" ? "prompt.lie_down" : "prompt.sit";
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
let key = def;
|
|
1120
|
+
if (i.label) {
|
|
1121
|
+
if (state && table[`${i.label}.${state}`] !== void 0) key = `${i.label}.${state}`;
|
|
1122
|
+
else if (table[i.label] !== void 0) key = i.label;
|
|
1123
|
+
}
|
|
1124
|
+
const vars = { name: typeName(o), circuit: a.do === "toggle_breaker" ? a.name ?? a.circuit : "" };
|
|
1125
|
+
const label = t(key, vars);
|
|
1126
|
+
let verb;
|
|
1127
|
+
let object;
|
|
1128
|
+
if (table[`${key}.verb`] !== void 0) {
|
|
1129
|
+
verb = t(`${key}.verb`, vars);
|
|
1130
|
+
object = table[`${key}.object`] ? t(`${key}.object`, vars) : vars.name;
|
|
1131
|
+
} else {
|
|
1132
|
+
const sp = label.indexOf(" ");
|
|
1133
|
+
verb = sp < 0 ? label : label.slice(0, sp);
|
|
1134
|
+
object = sp < 0 ? vars.name : label.slice(sp + 1);
|
|
1135
|
+
}
|
|
1136
|
+
return { label, verb, object };
|
|
1137
|
+
}
|
|
1138
|
+
function unavailableReason(o, i) {
|
|
1139
|
+
const a = i.action;
|
|
1140
|
+
if (!requirementMet(i)) return "requires_open";
|
|
1141
|
+
switch (a.do) {
|
|
1142
|
+
case "toggle_part": {
|
|
1143
|
+
const p = o.parts.get(a.part);
|
|
1144
|
+
return p && gated(o, p) ? "requires_open" : null;
|
|
1145
|
+
}
|
|
1146
|
+
case "open_container": {
|
|
1147
|
+
const c = containerOf(o, a.container);
|
|
1148
|
+
const p = c?.access ? o.parts.get(c.access) : void 0;
|
|
1149
|
+
return p && gated(o, p) ? "requires_open" : null;
|
|
1150
|
+
}
|
|
1151
|
+
case "toggle_breaker": {
|
|
1152
|
+
const rocker = a.part ? o.parts.get(a.part) : void 0;
|
|
1153
|
+
return rocker && gated(o, rocker) ? "requires_open" : null;
|
|
1154
|
+
}
|
|
1155
|
+
case "take":
|
|
1156
|
+
case "read":
|
|
1157
|
+
return accessible(o.id, a.container) ? null : "requires_open";
|
|
1158
|
+
case "ignite":
|
|
1159
|
+
return o.burners.get(a.burner)?.lit ? "already_lit" : null;
|
|
1160
|
+
case "toggle_burner": {
|
|
1161
|
+
const b = o.burners.get(a.burner);
|
|
1162
|
+
return !b?.spec.needsIgnition || b.lit ? null : "needs_ignition";
|
|
1163
|
+
}
|
|
1164
|
+
case "cycle_device": {
|
|
1165
|
+
const d = o.devices.get(a.device);
|
|
1166
|
+
if (!d || !d.on) return "turn_on_first";
|
|
1167
|
+
return devicePowered(o, d) ? null : "no_power";
|
|
1168
|
+
}
|
|
1169
|
+
default:
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function requirementMet(i) {
|
|
1174
|
+
if (!i.requires) return true;
|
|
1175
|
+
const p = objs.get(i.requires.object)?.parts.get(i.requires.part);
|
|
1176
|
+
return !!p && targetIsOpen(p);
|
|
1177
|
+
}
|
|
1178
|
+
function interactablesOf(o) {
|
|
1179
|
+
return o.interactions.map((i) => {
|
|
1180
|
+
const id = gid(o, i.id);
|
|
1181
|
+
const p = promptOf(o, i);
|
|
1182
|
+
const item = {
|
|
1183
|
+
id,
|
|
1184
|
+
object: o.id,
|
|
1185
|
+
kind: i.action.do,
|
|
1186
|
+
pos: toWorld(o, i.pos),
|
|
1187
|
+
radius: i.radius ?? DEFAULT_RADIUS,
|
|
1188
|
+
label: p.label,
|
|
1189
|
+
verb: p.verb,
|
|
1190
|
+
noun: p.object,
|
|
1191
|
+
available: true,
|
|
1192
|
+
act: (actor) => act(id, actor)
|
|
1193
|
+
};
|
|
1194
|
+
const reason = unavailableReason(o, i) ?? (options.canInteract?.(item, DEFAULT_ACTOR) === false ? "locked" : null);
|
|
1195
|
+
if (reason) {
|
|
1196
|
+
item.available = false;
|
|
1197
|
+
item.reason = reason;
|
|
1198
|
+
}
|
|
1199
|
+
return { item, spec: i, o };
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
function interactables() {
|
|
1203
|
+
const out = [];
|
|
1204
|
+
for (const o of objs.values()) for (const e of interactablesOf(o)) out.push(e.item);
|
|
1205
|
+
return out;
|
|
1206
|
+
}
|
|
1207
|
+
function target(eye, opts = {}) {
|
|
1208
|
+
const reach = opts.reach ?? 1.8;
|
|
1209
|
+
const cone = opts.cone ?? 0.45;
|
|
1210
|
+
const dir = [-Math.sin(eye.yaw) * Math.cos(eye.pitch), Math.sin(eye.pitch), -Math.cos(eye.yaw) * Math.cos(eye.pitch)];
|
|
1211
|
+
let best = null;
|
|
1212
|
+
let bestScore = Infinity;
|
|
1213
|
+
for (const o of objs.values()) {
|
|
1214
|
+
for (const e of interactablesOf(o)) {
|
|
1215
|
+
if (!e.item.available && !opts.includeUnavailable) continue;
|
|
1216
|
+
if (opts.kinds && !opts.kinds.includes(e.item.kind)) continue;
|
|
1217
|
+
if (opts.filter && !opts.filter(e.item)) continue;
|
|
1218
|
+
const v = [e.item.pos[0] - eye.x, e.item.pos[1] - eye.y, e.item.pos[2] - eye.z];
|
|
1219
|
+
const d = Math.hypot(v[0], v[1], v[2]);
|
|
1220
|
+
if (d > reach) continue;
|
|
1221
|
+
const cos = d < 1e-6 ? 1 : clamp((v[0] * dir[0] + v[1] * dir[1] + v[2] * dir[2]) / d, -1, 1);
|
|
1222
|
+
const angle = Math.acos(cos);
|
|
1223
|
+
if (angle > cone) continue;
|
|
1224
|
+
const score = angle / cone * 0.6 + d / reach * 0.4;
|
|
1225
|
+
if (score < bestScore) {
|
|
1226
|
+
bestScore = score;
|
|
1227
|
+
best = e.item;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return best;
|
|
1232
|
+
}
|
|
1233
|
+
function act(interactableId, actor = DEFAULT_ACTOR) {
|
|
1234
|
+
let found = null;
|
|
1235
|
+
for (const o of objs.values()) {
|
|
1236
|
+
if (!interactableId.startsWith(o.id + ":")) continue;
|
|
1237
|
+
const local = interactableId.slice(o.id.length + 1);
|
|
1238
|
+
if (!o.interactions.some((i) => i.id === local)) continue;
|
|
1239
|
+
found = interactablesOf(o).find((e) => e.spec.id === local) ?? null;
|
|
1240
|
+
if (found) break;
|
|
1241
|
+
}
|
|
1242
|
+
if (!found) return false;
|
|
1243
|
+
if (!requirementMet(found.spec)) return false;
|
|
1244
|
+
if (options.canInteract?.(found.item, actor) === false) return false;
|
|
1245
|
+
return execute(found.o, found.spec.action, actor);
|
|
1246
|
+
}
|
|
1247
|
+
function execute(o, a, actor) {
|
|
1248
|
+
switch (a.do) {
|
|
1249
|
+
case "toggle_part": {
|
|
1250
|
+
const p = o.parts.get(a.part);
|
|
1251
|
+
if (!p || gated(o, p)) return false;
|
|
1252
|
+
setPartTarget(o, a.part, targetIsOpen(p) ? 0 : 1);
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
case "open_container": {
|
|
1256
|
+
const c = containerOf(o, a.container);
|
|
1257
|
+
if (!c) return false;
|
|
1258
|
+
if (c.access) return execute(o, { do: "toggle_part", part: c.access }, actor);
|
|
1259
|
+
emit({ type: "container", object: o.id, container: c.id, open: true });
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
case "take": {
|
|
1263
|
+
if (!accessible(o.id, a.container)) return false;
|
|
1264
|
+
const list = o.contents.get(a.container);
|
|
1265
|
+
if (!list) return false;
|
|
1266
|
+
const slot = a.slot ?? list.findIndex((s) => s !== null);
|
|
1267
|
+
if (slot < 0 || !list[slot]) {
|
|
1268
|
+
message("empty");
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
return take(o.id, a.container, slot, actor) !== null;
|
|
1272
|
+
}
|
|
1273
|
+
case "read": {
|
|
1274
|
+
if (!accessible(o.id, a.container)) return false;
|
|
1275
|
+
const stack = o.contents.get(a.container)?.[a.slot];
|
|
1276
|
+
if (!stack) {
|
|
1277
|
+
message("empty");
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
const key = typeof stack.data?.text === "string" ? stack.data.text : null;
|
|
1281
|
+
if (!key) return false;
|
|
1282
|
+
message(key);
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
case "toggle_device": {
|
|
1286
|
+
const d = o.devices.get(a.device);
|
|
1287
|
+
if (!d) return false;
|
|
1288
|
+
setDeviceOn(o, d, !d.on);
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
case "cycle_device": {
|
|
1292
|
+
const d = o.devices.get(a.device);
|
|
1293
|
+
if (!d || !d.on) return false;
|
|
1294
|
+
if (!devicePowered(o, d)) {
|
|
1295
|
+
message("no_power");
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
d.channel = (d.channel + 1) % Math.max(1, d.spec.channels ?? 1);
|
|
1299
|
+
emit({ type: "device", object: o.id, device: d.spec.id, on: true, powered: true, channel: d.channel });
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
case "toggle_switch":
|
|
1303
|
+
return toggleSwitch(o, a.switch);
|
|
1304
|
+
case "toggle_breaker": {
|
|
1305
|
+
const rocker = a.part ? o.parts.get(a.part) : void 0;
|
|
1306
|
+
if (rocker && gated(o, rocker)) return false;
|
|
1307
|
+
const on = !breaker(a.circuit);
|
|
1308
|
+
if (a.part) setPartTarget(o, a.part, on ? 1 : 0);
|
|
1309
|
+
world.power.setBreaker(a.circuit, on);
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
case "toggle_fixture":
|
|
1313
|
+
return toggleFixture(o, a.fixture);
|
|
1314
|
+
case "toggle_burner":
|
|
1315
|
+
return toggleBurner(o, a.burner);
|
|
1316
|
+
case "ignite":
|
|
1317
|
+
return ignite(o, a.burner);
|
|
1318
|
+
case "toggle_fuel":
|
|
1319
|
+
return toggleFuel(o, a.fuel);
|
|
1320
|
+
case "sit": {
|
|
1321
|
+
const mine = seats.get(actor);
|
|
1322
|
+
if (mine && mine.object === o.id && mine.seat === a.seat) {
|
|
1323
|
+
stand(actor);
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
return sit(o.id, a.seat, actor) !== null;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function emitters() {
|
|
1331
|
+
const out = [];
|
|
1332
|
+
for (const o of objs.values()) {
|
|
1333
|
+
for (const e of o.spec.emitters ?? []) {
|
|
1334
|
+
let intensity = 0;
|
|
1335
|
+
const b = o.burners.get(e.source);
|
|
1336
|
+
const fx = o.fixtures.get(e.source);
|
|
1337
|
+
const dev = o.devices.get(e.source);
|
|
1338
|
+
const fuel = o.fuels.get(e.source);
|
|
1339
|
+
if (b) {
|
|
1340
|
+
const ref = resolveFuel(o, b);
|
|
1341
|
+
const frac = ref ? clamp(ref.f.level / ref.f.spec.capacity, 0, 1) : 0;
|
|
1342
|
+
if (b.lit) intensity = 0.35 + 0.65 * frac;
|
|
1343
|
+
else if (e.kind === "smoke" && b.smoke > 0) intensity = 0.6 * (b.smoke / SMOKE_TAIL);
|
|
1344
|
+
} else if (fx) {
|
|
1345
|
+
if (fx.on && fx.flowing) intensity = fx.frac;
|
|
1346
|
+
} else if (dev) {
|
|
1347
|
+
if (dev.on && devicePowered(o, dev)) intensity = 1;
|
|
1348
|
+
} else if (fuel) {
|
|
1349
|
+
const lit = [...o.burners.values()].some((bb) => bb.lit && resolveFuel(o, bb)?.f === fuel);
|
|
1350
|
+
if (lit) intensity = 0.35 + 0.65 * clamp(fuel.level / fuel.spec.capacity, 0, 1);
|
|
1351
|
+
}
|
|
1352
|
+
if (intensity <= 0) continue;
|
|
1353
|
+
const dir = rotY(e.dir ?? [0, 1, 0], o.public.transform.ry);
|
|
1354
|
+
out.push({ id: gid(o, e.id), object: o.id, kind: e.kind, pos: toWorld(o, e.pos), dir, size: e.size ?? 0.3, ...e.reach !== void 0 ? { reach: e.reach } : {}, intensity: clamp(intensity, 0, 1) });
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return out;
|
|
1358
|
+
}
|
|
1359
|
+
function mirrors(eye, maxDistance = 10, count = 4) {
|
|
1360
|
+
const out = [];
|
|
1361
|
+
for (const o of objs.values()) {
|
|
1362
|
+
for (const m of o.spec.mirrors ?? []) {
|
|
1363
|
+
const center = toWorld(o, applyPart(o, m.part, m.center));
|
|
1364
|
+
const f = partFrame(o, m.part);
|
|
1365
|
+
const ln = f && f.angle !== 0 ? rotateAxis(m.normal, f.axis, f.angle) : m.normal;
|
|
1366
|
+
const normal = rotY(ln, o.public.transform.ry);
|
|
1367
|
+
const len = Math.hypot(normal[0], normal[1], normal[2]) || 1;
|
|
1368
|
+
const n = [normal[0] / len, normal[1] / len, normal[2] / len];
|
|
1369
|
+
const to = [eye[0] - center[0], eye[1] - center[1], eye[2] - center[2]];
|
|
1370
|
+
if (n[0] * to[0] + n[1] * to[1] + n[2] * to[2] <= 0) continue;
|
|
1371
|
+
const distance = Math.hypot(to[0], to[1], to[2]);
|
|
1372
|
+
if (distance > maxDistance) continue;
|
|
1373
|
+
out.push({ id: gid(o, m.id), center, normal: n, w: m.w, h: m.h, distance });
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return out.sort((a, b) => a.distance - b.distance).slice(0, count);
|
|
1377
|
+
}
|
|
1378
|
+
function colliders(opts = {}) {
|
|
1379
|
+
const out = [];
|
|
1380
|
+
const fixed = [];
|
|
1381
|
+
for (const o of objs.values()) {
|
|
1382
|
+
const tr = o.public.transform;
|
|
1383
|
+
for (const c of o.spec.colliders ?? []) {
|
|
1384
|
+
if (c.part && c.offWhenOpen && (o.parts.get(c.part)?.t ?? 0) > 0.5) continue;
|
|
1385
|
+
let extra = 0;
|
|
1386
|
+
if (c.part) {
|
|
1387
|
+
const f = partFrame(o, c.part);
|
|
1388
|
+
if (f && f.angle !== 0 && Math.abs(f.axis[1]) >= 0.5) extra = f.angle * Math.sign(f.axis[1]);
|
|
1389
|
+
}
|
|
1390
|
+
const center = toWorld(o, applyPart(o, c.part, c.center));
|
|
1391
|
+
(c.part ? out : fixed).push({
|
|
1392
|
+
id: gid(o, c.id),
|
|
1393
|
+
object: o.id,
|
|
1394
|
+
cx: center[0],
|
|
1395
|
+
cz: center[2],
|
|
1396
|
+
hw: c.half[0],
|
|
1397
|
+
hd: c.half[2],
|
|
1398
|
+
ry: tr.ry + (c.ry ?? 0) + extra,
|
|
1399
|
+
y0: center[1] - c.half[1],
|
|
1400
|
+
y1: center[1] + c.half[1]
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (opts.merge) return [...mergeColliders(fixed, { gap: opts.gap, slack: opts.slack }), ...out];
|
|
1405
|
+
return [...fixed, ...out];
|
|
1406
|
+
}
|
|
1407
|
+
function update(dt) {
|
|
1408
|
+
if (!(dt > 0)) return;
|
|
1409
|
+
time += dt;
|
|
1410
|
+
for (const o of objs.values()) {
|
|
1411
|
+
for (const p of o.parts.values()) {
|
|
1412
|
+
if (p.t === p.target) continue;
|
|
1413
|
+
if (gated(o, p) && p.target > p.t) continue;
|
|
1414
|
+
const step = (p.spec.motion.speed ?? 2.5) * dt;
|
|
1415
|
+
const next = p.t < p.target ? Math.min(p.target, p.t + step) : Math.max(p.target, p.t - step);
|
|
1416
|
+
writeT(o, p, next);
|
|
1417
|
+
if (next === p.target) emit({ type: "part", object: o.id, part: p.spec.id, t: next, target: p.target });
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
stepSoft(dt);
|
|
1421
|
+
for (const o of objs.values()) {
|
|
1422
|
+
for (const b of o.burners.values()) {
|
|
1423
|
+
if (b.lit) {
|
|
1424
|
+
const ref = resolveFuel(o, b);
|
|
1425
|
+
if (!ref || !valveOpen(ref.o, ref.f)) {
|
|
1426
|
+
extinguish(o, b, !!b.spec.needsIgnition);
|
|
1427
|
+
} else {
|
|
1428
|
+
ref.f.level = Math.max(0, ref.f.level - b.spec.rate * dt);
|
|
1429
|
+
if (ref.f.level <= 0) extinguish(o, b, !!b.spec.needsIgnition);
|
|
1430
|
+
}
|
|
1431
|
+
} else if (b.on && !b.spec.needsIgnition && canBurn(b, resolveFuel(o, b))) {
|
|
1432
|
+
b.lit = true;
|
|
1433
|
+
b.smoke = 0;
|
|
1434
|
+
emitBurner(o, b);
|
|
1435
|
+
} else if (b.smoke > 0) {
|
|
1436
|
+
b.smoke = Math.max(0, b.smoke - dt);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (rain > 0) {
|
|
1440
|
+
for (const tk of o.tanks.values()) {
|
|
1441
|
+
if (tk.spec.rainArea) tk.level = Math.min(tk.spec.capacity, tk.level + rain * tk.spec.rainArea / 3600 * dt);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
for (const tk of o.tanks.values()) {
|
|
1445
|
+
const st = o.hp.get(tk.spec.id);
|
|
1446
|
+
if (st && st.hp < st.max && tk.level > 0) {
|
|
1447
|
+
const rate = 0.5 + 2.5 * (1 - st.hp / st.max);
|
|
1448
|
+
tk.level = Math.max(0, tk.level - rate * dt);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
for (const o of objs.values()) {
|
|
1453
|
+
for (const d of o.devices.values()) {
|
|
1454
|
+
const fx = d.spec.fixture ? o.fixtures.get(d.spec.fixture) : void 0;
|
|
1455
|
+
if (!fx) continue;
|
|
1456
|
+
const run = d.on && devicePowered(o, d);
|
|
1457
|
+
if (fx.on === run) continue;
|
|
1458
|
+
fx.on = run;
|
|
1459
|
+
if (!run) {
|
|
1460
|
+
fx.flowing = false;
|
|
1461
|
+
fx.frac = 0;
|
|
1462
|
+
emitFixture(o, fx);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
for (const fx of o.fixtures.values()) stepFixture(o, fx, dt);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function add(id, spec, transform) {
|
|
1469
|
+
if (objs.has(id)) throw new Error(`Object "${id}" already exists`);
|
|
1470
|
+
const pub = { id, spec, transform: { ...transform }, parts: {} };
|
|
1471
|
+
const o = {
|
|
1472
|
+
id,
|
|
1473
|
+
public: pub,
|
|
1474
|
+
spec,
|
|
1475
|
+
parts: /* @__PURE__ */ new Map(),
|
|
1476
|
+
contents: /* @__PURE__ */ new Map(),
|
|
1477
|
+
devices: /* @__PURE__ */ new Map(),
|
|
1478
|
+
fuels: /* @__PURE__ */ new Map(),
|
|
1479
|
+
burners: /* @__PURE__ */ new Map(),
|
|
1480
|
+
tanks: /* @__PURE__ */ new Map(),
|
|
1481
|
+
fixtures: /* @__PURE__ */ new Map(),
|
|
1482
|
+
soft: /* @__PURE__ */ new Map(),
|
|
1483
|
+
hp: /* @__PURE__ */ new Map(),
|
|
1484
|
+
interactions: spec.interactions ?? deriveInteractions(spec)
|
|
1485
|
+
};
|
|
1486
|
+
for (const p of spec.parts ?? []) {
|
|
1487
|
+
const t0 = clamp(p.t0 ?? 0, 0, 1);
|
|
1488
|
+
o.parts.set(p.id, { spec: p, t: t0, target: t0, maxT: 1 });
|
|
1489
|
+
pub.parts[p.id] = t0;
|
|
1490
|
+
}
|
|
1491
|
+
for (const c of spec.containers ?? []) {
|
|
1492
|
+
let list;
|
|
1493
|
+
if (c.contents) list = c.slots.map((_, i) => cloneStack(c.contents?.[i] ?? null));
|
|
1494
|
+
else if (c.loot) list = generateLoot(options.loot, c.loot, c.slots, `${seed}|${id}|${c.id}`, options.items);
|
|
1495
|
+
else list = c.slots.map(() => null);
|
|
1496
|
+
o.contents.set(c.id, list);
|
|
1497
|
+
}
|
|
1498
|
+
for (const d of spec.devices ?? []) {
|
|
1499
|
+
o.devices.set(d.id, { spec: d, on: !!d.on, channel: 0 });
|
|
1500
|
+
if (d.on && d.part) {
|
|
1501
|
+
const p = o.parts.get(d.part);
|
|
1502
|
+
if (p) {
|
|
1503
|
+
p.target = p.maxT;
|
|
1504
|
+
writeT(o, p, p.maxT);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
for (const f of spec.fuels ?? []) o.fuels.set(f.id, { spec: f, level: clamp(f.level ?? f.capacity, 0, f.capacity) });
|
|
1509
|
+
for (const b of spec.burners ?? []) o.burners.set(b.id, { spec: b, on: false, lit: false, smoke: 0 });
|
|
1510
|
+
for (const tk of spec.tanks ?? []) o.tanks.set(tk.id, { spec: tk, level: clamp(tk.level ?? tk.capacity, 0, tk.capacity) });
|
|
1511
|
+
for (const f of spec.fixtures ?? []) o.fixtures.set(f.id, { spec: f, on: false, flowing: false, frac: 0, timer: 0 });
|
|
1512
|
+
let n = 0;
|
|
1513
|
+
for (const s of spec.soft ?? []) {
|
|
1514
|
+
o.soft.set(s.id, { spec: s, ax: 0, az: 0, vax: 0, vaz: 0, phase: n++ * 2.399963 % (Math.PI * 2) });
|
|
1515
|
+
}
|
|
1516
|
+
for (const d of spec.damage ?? []) o.hp.set(d.target, { max: d.hp, hp: d.hp });
|
|
1517
|
+
objs.set(id, o);
|
|
1518
|
+
if (!defaultTankExplicit && defaultTank === null && o.tanks.size) defaultTank = gid(o, [...o.tanks.keys()][0]);
|
|
1519
|
+
rebuildPlugs();
|
|
1520
|
+
return pub;
|
|
1521
|
+
}
|
|
1522
|
+
function remove(id) {
|
|
1523
|
+
const o = objs.get(id);
|
|
1524
|
+
if (!o) return;
|
|
1525
|
+
for (const [actor, s] of [...seats]) if (s.object === id) seats.delete(actor);
|
|
1526
|
+
objs.delete(id);
|
|
1527
|
+
if (defaultTank && defaultTank.startsWith(id + ":")) defaultTank = null;
|
|
1528
|
+
rebuildPlugs();
|
|
1529
|
+
}
|
|
1530
|
+
function serialize() {
|
|
1531
|
+
const objects = {};
|
|
1532
|
+
for (const o of objs.values()) {
|
|
1533
|
+
objects[o.id] = {
|
|
1534
|
+
parts: Object.fromEntries([...o.parts].map(([k, p]) => [k, p.t])),
|
|
1535
|
+
contents: Object.fromEntries([...o.contents].map(([k, v]) => [k, v.map(cloneStack)])),
|
|
1536
|
+
devices: Object.fromEntries([...o.devices].map(([k, d]) => [k, { on: d.on, channel: d.channel }])),
|
|
1537
|
+
fuels: Object.fromEntries([...o.fuels].map(([k, f]) => [k, { level: f.level, open: valveOpen(o, f) }])),
|
|
1538
|
+
burners: Object.fromEntries([...o.burners].map(([k, b]) => [k, { on: b.on, lit: b.lit }])),
|
|
1539
|
+
tanks: Object.fromEntries([...o.tanks].map(([k, tk]) => [k, tk.level])),
|
|
1540
|
+
fixtures: Object.fromEntries([...o.fixtures].map(([k, f]) => [k, f.on])),
|
|
1541
|
+
hp: Object.fromEntries([...o.hp].map(([k, h]) => [k, h.hp]))
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
version: 1,
|
|
1546
|
+
objects,
|
|
1547
|
+
grid,
|
|
1548
|
+
breakers: Object.fromEntries(breakers),
|
|
1549
|
+
seats: Object.fromEntries([...seats].map(([a, s]) => [a, { object: s.object, seat: s.seat }])),
|
|
1550
|
+
defaultTank
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function load(state) {
|
|
1554
|
+
for (const [id, st] of Object.entries(state.objects)) {
|
|
1555
|
+
const o = objs.get(id);
|
|
1556
|
+
if (!o) continue;
|
|
1557
|
+
for (const [k, v] of Object.entries(st.parts)) {
|
|
1558
|
+
const p = o.parts.get(k);
|
|
1559
|
+
if (p) {
|
|
1560
|
+
p.target = clamp(v, 0, p.maxT);
|
|
1561
|
+
writeT(o, p, p.target);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
for (const [k, v] of Object.entries(st.contents)) {
|
|
1565
|
+
const c = containerOf(o, k);
|
|
1566
|
+
if (c) o.contents.set(k, c.slots.map((_, i) => cloneStack(v[i] ?? null)));
|
|
1567
|
+
}
|
|
1568
|
+
for (const [k, v] of Object.entries(st.devices)) {
|
|
1569
|
+
const d = o.devices.get(k);
|
|
1570
|
+
if (d) {
|
|
1571
|
+
d.on = v.on;
|
|
1572
|
+
d.channel = v.channel;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
for (const [k, v] of Object.entries(st.fuels)) {
|
|
1576
|
+
const f = o.fuels.get(k);
|
|
1577
|
+
if (f) f.level = v.level;
|
|
1578
|
+
}
|
|
1579
|
+
for (const [k, v] of Object.entries(st.burners)) {
|
|
1580
|
+
const b = o.burners.get(k);
|
|
1581
|
+
if (b) {
|
|
1582
|
+
b.on = v.on;
|
|
1583
|
+
b.lit = v.lit;
|
|
1584
|
+
b.smoke = 0;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
for (const [k, v] of Object.entries(st.tanks)) {
|
|
1588
|
+
const tk = o.tanks.get(k);
|
|
1589
|
+
if (tk) tk.level = v;
|
|
1590
|
+
}
|
|
1591
|
+
for (const [k, v] of Object.entries(st.fixtures)) {
|
|
1592
|
+
const f = o.fixtures.get(k);
|
|
1593
|
+
if (f) {
|
|
1594
|
+
f.on = v;
|
|
1595
|
+
f.flowing = false;
|
|
1596
|
+
f.frac = 0;
|
|
1597
|
+
f.timer = v && f.spec.kind === "flush" ? FLUSH_SECONDS : 0;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
for (const [k, v] of Object.entries(st.hp)) {
|
|
1601
|
+
const h = o.hp.get(k);
|
|
1602
|
+
if (h) h.hp = v;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
grid = state.grid;
|
|
1606
|
+
breakers.clear();
|
|
1607
|
+
for (const [k, v] of Object.entries(state.breakers)) breakers.set(k, v);
|
|
1608
|
+
seats.clear();
|
|
1609
|
+
for (const [a, s] of Object.entries(state.seats)) if (objs.has(s.object)) seats.set(a, { object: s.object, seat: s.seat });
|
|
1610
|
+
defaultTank = state.defaultTank;
|
|
1611
|
+
defaultTankExplicit = true;
|
|
1612
|
+
}
|
|
1613
|
+
const world = {
|
|
1614
|
+
add,
|
|
1615
|
+
remove,
|
|
1616
|
+
get: (id) => objs.get(id)?.public,
|
|
1617
|
+
objects: () => [...objs.values()].map((o) => o.public),
|
|
1618
|
+
update,
|
|
1619
|
+
interactables,
|
|
1620
|
+
target,
|
|
1621
|
+
act,
|
|
1622
|
+
partT: (objectId, partId) => objs.get(objectId)?.parts.get(partId)?.t ?? 0,
|
|
1623
|
+
setDevice,
|
|
1624
|
+
setFixture,
|
|
1625
|
+
setBurner,
|
|
1626
|
+
setPart: (objectId, partId, targetT, instant) => {
|
|
1627
|
+
const o = objs.get(objectId);
|
|
1628
|
+
if (o) setPartTarget(o, partId, targetT, instant);
|
|
1629
|
+
},
|
|
1630
|
+
fitSweep,
|
|
1631
|
+
contents,
|
|
1632
|
+
accessible,
|
|
1633
|
+
take,
|
|
1634
|
+
put,
|
|
1635
|
+
slotPosition,
|
|
1636
|
+
power: {
|
|
1637
|
+
get grid() {
|
|
1638
|
+
return grid;
|
|
1639
|
+
},
|
|
1640
|
+
set grid(on) {
|
|
1641
|
+
world.power.setGrid(on);
|
|
1642
|
+
},
|
|
1643
|
+
setGrid(on) {
|
|
1644
|
+
if (on === grid) return;
|
|
1645
|
+
const before = powerSnapshot();
|
|
1646
|
+
grid = on;
|
|
1647
|
+
emit({ type: "power", grid });
|
|
1648
|
+
emitPowerChanges(before);
|
|
1649
|
+
},
|
|
1650
|
+
setBreaker(circuit, on) {
|
|
1651
|
+
if (breaker(circuit) === on) {
|
|
1652
|
+
breakers.set(circuit, on);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const before = powerSnapshot();
|
|
1656
|
+
breakers.set(circuit, on);
|
|
1657
|
+
emitPowerChanges(before);
|
|
1658
|
+
},
|
|
1659
|
+
breaker,
|
|
1660
|
+
isPowered: (objectId, deviceId) => {
|
|
1661
|
+
const r = getDevice(objectId, deviceId);
|
|
1662
|
+
return r ? devicePowered(r.o, r.d) : false;
|
|
1663
|
+
},
|
|
1664
|
+
isOn: (objectId, deviceId) => getDevice(objectId, deviceId)?.d.on ?? false,
|
|
1665
|
+
pluggedInto: (objectId, deviceId) => plugs.get(`${objectId}:${deviceId}`) ?? null,
|
|
1666
|
+
channel: (objectId, deviceId) => getDevice(objectId, deviceId)?.d.channel ?? 0
|
|
1667
|
+
},
|
|
1668
|
+
fuelLevel: (objectId, fuelId) => objs.get(objectId)?.fuels.get(fuelId)?.level ?? 0,
|
|
1669
|
+
isLit: (objectId, burnerId) => objs.get(objectId)?.burners.get(burnerId)?.lit ?? false,
|
|
1670
|
+
water: {
|
|
1671
|
+
setDefaultTank(tankId) {
|
|
1672
|
+
defaultTank = tankId;
|
|
1673
|
+
defaultTankExplicit = true;
|
|
1674
|
+
},
|
|
1675
|
+
level(tankId) {
|
|
1676
|
+
return findGlobal(tankId, (oo, local) => oo.tanks.get(local))?.value.level ?? 0;
|
|
1677
|
+
},
|
|
1678
|
+
refill(tankId, liters) {
|
|
1679
|
+
const f = findGlobal(tankId, (oo, local) => oo.tanks.get(local));
|
|
1680
|
+
if (!f) return 0;
|
|
1681
|
+
const added = Math.max(0, Math.min(liters, f.value.spec.capacity - f.value.level));
|
|
1682
|
+
f.value.level += added;
|
|
1683
|
+
return added;
|
|
1684
|
+
},
|
|
1685
|
+
setRain(mm) {
|
|
1686
|
+
rain = Math.max(0, mm);
|
|
1687
|
+
},
|
|
1688
|
+
isFlowing: (objectId, fixtureId) => {
|
|
1689
|
+
const fx = objs.get(objectId)?.fixtures.get(fixtureId);
|
|
1690
|
+
return !!fx && fx.on && fx.flowing;
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
sit,
|
|
1694
|
+
stand,
|
|
1695
|
+
seatOf: (actor = DEFAULT_ACTOR) => {
|
|
1696
|
+
const s = seats.get(actor);
|
|
1697
|
+
return s ? { object: s.object, seat: s.seat } : null;
|
|
1698
|
+
},
|
|
1699
|
+
push,
|
|
1700
|
+
sway: (objectId, softId) => {
|
|
1701
|
+
const s = objs.get(objectId)?.soft.get(softId);
|
|
1702
|
+
return s ? { ax: s.ax, az: s.az } : { ax: 0, az: 0 };
|
|
1703
|
+
},
|
|
1704
|
+
setWind: (x, z) => {
|
|
1705
|
+
wind = [x, z];
|
|
1706
|
+
},
|
|
1707
|
+
emitters,
|
|
1708
|
+
mirrors,
|
|
1709
|
+
colliders,
|
|
1710
|
+
damage,
|
|
1711
|
+
hp: (objectId, target2) => options.destruction ? null : objs.get(objectId)?.hp.get(target2)?.hp ?? null,
|
|
1712
|
+
t,
|
|
1713
|
+
setLocale: (l) => {
|
|
1714
|
+
locale = l;
|
|
1715
|
+
table = resolveStrings(locale, options.strings);
|
|
1716
|
+
},
|
|
1717
|
+
serialize,
|
|
1718
|
+
load
|
|
1719
|
+
};
|
|
1720
|
+
return world;
|
|
1721
|
+
}
|
|
1722
|
+
export {
|
|
1723
|
+
createWorld,
|
|
1724
|
+
fitsSize,
|
|
1725
|
+
fitsSlot,
|
|
1726
|
+
format,
|
|
1727
|
+
generateLoot,
|
|
1728
|
+
hashString,
|
|
1729
|
+
localToWorld,
|
|
1730
|
+
locales,
|
|
1731
|
+
mergeColliders,
|
|
1732
|
+
pickLocale,
|
|
1733
|
+
poseOf,
|
|
1734
|
+
resolveStrings,
|
|
1735
|
+
rng,
|
|
1736
|
+
rotY,
|
|
1737
|
+
rotateAxis,
|
|
1738
|
+
worldToLocal
|
|
1739
|
+
};
|
|
1740
|
+
//# sourceMappingURL=index.js.map
|