@adatechnology/game-test-room 0.0.0-stage → 0.1.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 +110 -2
- package/dist/index.d.ts +419 -0
- package/dist/index.global.js +14733 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +4430 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,4430 @@
|
|
|
1
|
+
// src/room.ts
|
|
2
|
+
import { createDestruction, JOINT_DEFAULTS as JOINT_DEFAULTS2, resolveStrings as resolveDestructionStrings } from "@adatechnology/game-destruction";
|
|
3
|
+
import { createDestructionRenderer, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
4
|
+
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
5
|
+
import { createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
6
|
+
import { createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
7
|
+
import { checkAnimation, checkDestructible } from "@adatechnology/game-contracts";
|
|
8
|
+
|
|
9
|
+
// src/items.ts
|
|
10
|
+
import { buildFrame } from "@adatechnology/game-destruction";
|
|
11
|
+
|
|
12
|
+
// src/build-from-spec.ts
|
|
13
|
+
var MATERIAL_COLORS = {
|
|
14
|
+
steel: 9278620,
|
|
15
|
+
cast_iron: 4869458,
|
|
16
|
+
aluminium: 12896718,
|
|
17
|
+
copper: 12088122,
|
|
18
|
+
wood_soft: 12158287,
|
|
19
|
+
wood_hard: 8014374,
|
|
20
|
+
plywood: 13214826,
|
|
21
|
+
mdf: 11045732,
|
|
22
|
+
glass: 10278630,
|
|
23
|
+
tempered_glass: 10278630,
|
|
24
|
+
ceramic: 15328474,
|
|
25
|
+
concrete: 10132118,
|
|
26
|
+
brick: 10831662,
|
|
27
|
+
plaster: 15131092,
|
|
28
|
+
stone: 7828591,
|
|
29
|
+
plastic: 3897540,
|
|
30
|
+
rubber: 2039586,
|
|
31
|
+
fabric: 9063262,
|
|
32
|
+
foam: 14926936,
|
|
33
|
+
leather: 5978662,
|
|
34
|
+
paper: 15657695,
|
|
35
|
+
foliage: 4165434,
|
|
36
|
+
green_wood: 7031339,
|
|
37
|
+
cactus: 4095306,
|
|
38
|
+
flesh_fruit: 13781810,
|
|
39
|
+
wax: 15721141,
|
|
40
|
+
soil: 4862754
|
|
41
|
+
};
|
|
42
|
+
var TRANSPARENT = { glass: 0.38, tempered_glass: 0.38 };
|
|
43
|
+
function buildFromSpec(THREE, spec, options = {}) {
|
|
44
|
+
const root = new THREE.Group();
|
|
45
|
+
root.name = spec.name ?? "object";
|
|
46
|
+
const seg = options.segments ?? 12;
|
|
47
|
+
const shadows = options.shadows !== false;
|
|
48
|
+
const mats = /* @__PURE__ */ new Map();
|
|
49
|
+
const materialFor = (p) => {
|
|
50
|
+
const color = options.colors?.[p.id] ?? options.colors?.[p.material] ?? MATERIAL_COLORS[p.material] ?? 8947848;
|
|
51
|
+
const key = `${p.material}:${color}`;
|
|
52
|
+
let m = mats.get(key);
|
|
53
|
+
if (!m) {
|
|
54
|
+
const alpha = TRANSPARENT[p.material];
|
|
55
|
+
const metal = p.material === "steel" || p.material === "aluminium" || p.material === "copper" || p.material === "cast_iron";
|
|
56
|
+
m = new THREE.MeshStandardMaterial({
|
|
57
|
+
color,
|
|
58
|
+
roughness: alpha ? 0.1 : metal ? 0.45 : 0.85,
|
|
59
|
+
metalness: metal ? 0.7 : 0,
|
|
60
|
+
transparent: !!alpha,
|
|
61
|
+
opacity: alpha ?? 1,
|
|
62
|
+
depthWrite: !alpha
|
|
63
|
+
});
|
|
64
|
+
mats.set(key, m);
|
|
65
|
+
}
|
|
66
|
+
return m;
|
|
67
|
+
};
|
|
68
|
+
for (const p of spec.parts) {
|
|
69
|
+
let geo;
|
|
70
|
+
const s = p.shape;
|
|
71
|
+
if (s.kind === "box") geo = new THREE.BoxGeometry(s.size[0], s.size[1], s.size[2]);
|
|
72
|
+
else if (s.kind === "cylinder") geo = new THREE.CylinderGeometry(s.radius, s.radius, s.height, seg);
|
|
73
|
+
else geo = new THREE.SphereGeometry(s.radius, seg, Math.max(6, seg - 4));
|
|
74
|
+
const mesh = new THREE.Mesh(geo, materialFor(p));
|
|
75
|
+
mesh.name = p.id;
|
|
76
|
+
mesh.userData.destructionPart = p.id;
|
|
77
|
+
mesh.userData.destructionMaterial = p.material;
|
|
78
|
+
mesh.position.set(p.position[0], p.position[1], p.position[2]);
|
|
79
|
+
if (p.rotation) mesh.rotation.set(p.rotation[0], p.rotation[1], p.rotation[2], "XYZ");
|
|
80
|
+
mesh.castShadow = shadows && !TRANSPARENT[p.material];
|
|
81
|
+
mesh.receiveShadow = shadows;
|
|
82
|
+
root.add(mesh);
|
|
83
|
+
}
|
|
84
|
+
return root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/locale.ts
|
|
88
|
+
var en = {
|
|
89
|
+
items: {
|
|
90
|
+
chair: "Wooden chair",
|
|
91
|
+
sofa: "Sofa",
|
|
92
|
+
shelf: "Shelf with boards",
|
|
93
|
+
old_tv: "Old TV",
|
|
94
|
+
steel_sheet: "Steel sheet on a stand",
|
|
95
|
+
glass_pane: "Glass pane",
|
|
96
|
+
concrete_block: "Concrete block",
|
|
97
|
+
brick_column: "Brick column",
|
|
98
|
+
building_frame: "Building frame",
|
|
99
|
+
car_door: "Car door",
|
|
100
|
+
small_tree: "Small tree",
|
|
101
|
+
potted_plant: "Potted plant",
|
|
102
|
+
coffee_cup: "Cup of coffee",
|
|
103
|
+
glass_bottle: "Glass bottle",
|
|
104
|
+
wooden_crate: "Wooden crate",
|
|
105
|
+
steel_drum: "Steel drum",
|
|
106
|
+
plastic_bucket: "Plastic bucket",
|
|
107
|
+
jerrycan: "Jerrycan",
|
|
108
|
+
gas_bottle: "Gas bottle"
|
|
109
|
+
},
|
|
110
|
+
groups: { furniture: "Furniture", structure: "Structure", vehicles: "Vehicles", plants: "Plants", items: "Items", containers: "Containers" },
|
|
111
|
+
text: {
|
|
112
|
+
"locale.decimal": ".",
|
|
113
|
+
yes: "yes",
|
|
114
|
+
no: "no",
|
|
115
|
+
piece: "piece",
|
|
116
|
+
coords: "x {x} \xB7 z {z}",
|
|
117
|
+
"count.piece.one": "1 piece",
|
|
118
|
+
"count.piece.other": "{n} pieces",
|
|
119
|
+
"count.joint.one": "1 joint",
|
|
120
|
+
"count.joint.other": "{n} joints",
|
|
121
|
+
"count.dent.one": "1 dent",
|
|
122
|
+
"count.dent.other": "{n} dents",
|
|
123
|
+
"count.hole.one": "1 hole",
|
|
124
|
+
"count.hole.other": "{n} holes",
|
|
125
|
+
"count.crack.one": "1 crack",
|
|
126
|
+
"count.crack.other": "{n} cracks",
|
|
127
|
+
"joint.generic": "joint",
|
|
128
|
+
"lost.broken": "broke",
|
|
129
|
+
"lost.melted": "melted away",
|
|
130
|
+
"lost.ashes": "burned to ash",
|
|
131
|
+
"tool.none": "by hand only",
|
|
132
|
+
"arena.target_wall": "Target wall",
|
|
133
|
+
"arena.table": "Table",
|
|
134
|
+
"arena.shelf": "Shelf",
|
|
135
|
+
"surface.floor": "on the floor",
|
|
136
|
+
"surface.ground": "on the floor",
|
|
137
|
+
"surface.table": "on the table",
|
|
138
|
+
"by.break": "broken loose",
|
|
139
|
+
"by.tool": "undone with a tool",
|
|
140
|
+
"by.part_lost": "the part next to it was lost",
|
|
141
|
+
"dmg.ballistic": "ballistic",
|
|
142
|
+
"dmg.blunt": "blunt",
|
|
143
|
+
"dmg.slash": "slash",
|
|
144
|
+
"dmg.explosive": "blast",
|
|
145
|
+
"dmg.fire": "fire",
|
|
146
|
+
"dmg.heat": "heat",
|
|
147
|
+
"effect.caffeine": "caffeine",
|
|
148
|
+
"effect.alcohol": "alcohol",
|
|
149
|
+
"effect.poison": "poison",
|
|
150
|
+
"effect.disgust": "disgust",
|
|
151
|
+
"ev.dent": "{where}: dent of {mm} mm, {hp}",
|
|
152
|
+
"ev.hole.through": "{where}: {mm} mm hole went through, {hp}",
|
|
153
|
+
"ev.hole.stuck": "{where}: {mm} mm hole, did not go through, {hp}",
|
|
154
|
+
"ev.dents": "{where}: {n} dents, deepest {mm} mm, {hp}",
|
|
155
|
+
"ev.holes": "{where}: {n} holes of {mm} mm, {thr} went through, {hp}",
|
|
156
|
+
"ev.leak.tail": ", leaking {liquid} at {rate} l/s",
|
|
157
|
+
"ev.leak.alone": "{where}: leaking {liquid} at {rate} l/s",
|
|
158
|
+
"ev.crack": "{where}: cracked, {rays} rays up to {cm} cm, {hp}",
|
|
159
|
+
"ev.ricochet": "{where}: bullet ricocheted, {energy} J left",
|
|
160
|
+
"ev.fracture.shatter": "{where}: shattered into {n} shards",
|
|
161
|
+
"ev.fracture.crumble": "{where}: crumbled into {n} chunks",
|
|
162
|
+
"ev.fracture.splinter": "{where}: splintered into {n} pieces",
|
|
163
|
+
"ev.fracture.tear": "{where}: torn into {n} scraps",
|
|
164
|
+
"ev.fracture.snap": "{where}: snapped into {n} pieces",
|
|
165
|
+
"ev.fracture.none": "{where}: broke into {n} pieces",
|
|
166
|
+
"ev.detach": "{obj}: {a} came off {b} ({by}); the object now has {pieces}",
|
|
167
|
+
"ev.detach.many.break": "{obj}: {joints} broke loose; the object now has {pieces}",
|
|
168
|
+
"ev.detach.many.tool": "{obj}: {joints} undone with a tool; the object now has {pieces}",
|
|
169
|
+
"ev.lost.tail": " and came loose from {joints}; the object now has {pieces}",
|
|
170
|
+
"ev.lost.alone": "{where}: {verb} and came loose from {joints}; the object now has {pieces}",
|
|
171
|
+
"ev.cracks": "{where}: cracking under the load, {n} cracks so far, longest {cm} cm, {hp}",
|
|
172
|
+
"ev.disassemble": "{obj}: undoing the {kind} between {a} and {b} with {tool}: {pct}%",
|
|
173
|
+
"ev.ignite": "{where}: caught fire at {temp} \xB0C",
|
|
174
|
+
"ev.ignite.many": "{obj}: {n} parts caught fire at {temp} \xB0C ({parts})",
|
|
175
|
+
"ev.extinguish": "{where}: fire put out",
|
|
176
|
+
"ev.extinguish.many": "{obj}: fire put out on {n} parts ({parts})",
|
|
177
|
+
"ev.burnout": "{where}: burned to ash",
|
|
178
|
+
"ev.melt": "{where}: melting, {pct}% melted away",
|
|
179
|
+
"ev.drip": "{where}: dripping molten material",
|
|
180
|
+
"ev.explode": "{where}: exploded, {energy} J within {radius} m",
|
|
181
|
+
"ev.destroyed": "{obj}: destroyed",
|
|
182
|
+
"ev.damage": "{where}: {type} damage {amount}, {hp}",
|
|
183
|
+
"ev.damage.overload": "{where}: crushed by the weight it carries, lost {amount} hp so far, {hp}",
|
|
184
|
+
"ev.stress": "{where}: overloaded at {stress}% of its capacity, carrying {load} kg",
|
|
185
|
+
"ev.collapse": "{obj}: collapsed, {pieces} fell ({mass} kg): {parts}",
|
|
186
|
+
"ev.stain_ignite": "The {liquid} stain at x {x} \xB7 z {z} caught fire and will burn for about {seconds} s",
|
|
187
|
+
"ev.stain_burned": "The burning stain burned itself out",
|
|
188
|
+
"ev.fire_dries": "Fire dried the wet stains nearby",
|
|
189
|
+
"fl.fill": "{who}: filled with {liquid} (+{liters} l)",
|
|
190
|
+
"fl.pour": "{who}: pouring {liquid} at {rate} l/s",
|
|
191
|
+
"fl.spill": "{who}: spilled {liquid} over the lip at {rate} l/s",
|
|
192
|
+
"fl.leak": "{who}: leaking {liquid} at {rate} l/s",
|
|
193
|
+
"fl.drink": "{who}: drank {liters} l, thirst +{thirst}",
|
|
194
|
+
"fl.burn": "too hot: burned the mouth",
|
|
195
|
+
"fl.empty": "{who}: empty",
|
|
196
|
+
"fl.splash": "Splash of {liquid}",
|
|
197
|
+
"fl.stain": "Stain of {liquid} {surface} at {where}",
|
|
198
|
+
"fl.stains": "{n} stains of {liquid} {surface}, the latest at {where}",
|
|
199
|
+
"fl.dried": "A stain dried up",
|
|
200
|
+
"fl.dried.mark": "A stain dried and left a mark",
|
|
201
|
+
"fl.dried.known": "The {liquid} stain {surface} at {where} dried up",
|
|
202
|
+
"fl.dried.known.mark": "The {liquid} stain {surface} at {where} dried and left a mark",
|
|
203
|
+
"fl.dried.many": "{n} {liquid} stains dried up",
|
|
204
|
+
"fl.dried.many.mark": "{n} {liquid} stains dried and left marks",
|
|
205
|
+
"fl.mix": "{who}: {from} mixed into {into}",
|
|
206
|
+
"act.anim_start": '{name}: animation "{clip}" ({seconds} s)',
|
|
207
|
+
"act.anim_event": '{name}: "{clip}" event {type}',
|
|
208
|
+
"act.anim_end": '{name}: animation "{clip}" finished',
|
|
209
|
+
"act.module_loaded": 'Module "{name}" loaded: {items} item(s), {scenarios} scenario(s), contract checks: {result}',
|
|
210
|
+
"mod.all_ok": "all passed",
|
|
211
|
+
"mod.some_failed": "some FAILED",
|
|
212
|
+
"mod.destructible_ok": "{name}: destructible contract ok ({parts} parts, hit and state well-formed)",
|
|
213
|
+
"mod.destructible_fail": "{name}: destructible contract broken: {problems}",
|
|
214
|
+
"mod.animation_ok": '{name}: animation "{clip}" ok ({seconds} s)',
|
|
215
|
+
"mod.animation_fail": '{name}: animation "{clip}" broken: {problems}',
|
|
216
|
+
"ui.animations": "Animations",
|
|
217
|
+
"ui.animations_aim": "Aim at an item or hold it to see its animations.",
|
|
218
|
+
"ui.animations_none": "This item brings no animations.",
|
|
219
|
+
"act.spawn": "Spawned {name} at x {x} \xB7 z {z}",
|
|
220
|
+
"act.clear": "Cleared the room",
|
|
221
|
+
"act.reset": "Repaired every object",
|
|
222
|
+
"act.tool": "Equipped {weapon}",
|
|
223
|
+
"act.shoot.hit": "{weapon}: shot, {energy} J, hit {where} at {dist} m",
|
|
224
|
+
"act.shoot.miss": "{weapon}: shot, {energy} J, hit nothing",
|
|
225
|
+
"act.shoot.pellets": "{weapon}: shot, {n} projectiles of up to {energy} J, {hits} hit {where} at {dist} m",
|
|
226
|
+
"act.melee.hit": "{weapon}: strike, {energy} J, hit {where} at {dist} m",
|
|
227
|
+
"act.melee.miss": "{weapon}: struck the air",
|
|
228
|
+
"act.flame.hit": "{weapon}: flame at {temp} \xB0C on {where}",
|
|
229
|
+
"act.flame.air": "{weapon}: flame, nothing in range",
|
|
230
|
+
"act.heat.hit": "{weapon}: heating {where} at {temp} \xB0C",
|
|
231
|
+
"act.heat.air": "{weapon}: nothing in range",
|
|
232
|
+
"act.throw.grenade": "{weapon}: thrown, fuse {fuse} s",
|
|
233
|
+
"act.throw.molotov": "{weapon}: thrown, breaks on impact",
|
|
234
|
+
"act.explode": "{weapon}: exploded, {energy} J within {radius} m, {n} objects in range",
|
|
235
|
+
"act.molotov_fire": "{weapon}: fire on the ground, radius {radius} m for {seconds} s",
|
|
236
|
+
"act.tool.none": "{weapon}: nothing in reach",
|
|
237
|
+
"act.tool.nojoint": "{weapon}: no joint near {where}",
|
|
238
|
+
"act.tool.refused": "{weapon} cannot undo the {kind} of {obj}; it needs: {need}",
|
|
239
|
+
"act.extinguish.hit": "{weapon}: foam on {what}",
|
|
240
|
+
"act.extinguish.air": "{weapon}: foam, nothing in range",
|
|
241
|
+
"act.grab": "Grabbed {name} ({kg} kg)",
|
|
242
|
+
"act.drop": "Dropped {name}",
|
|
243
|
+
"act.throw": "Threw {name} at {speed} m/s",
|
|
244
|
+
"act.landing": "{name} landed at {speed} m/s: {energy} J",
|
|
245
|
+
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
246
|
+
"act.use_item": "{name}: {use}",
|
|
247
|
+
"act.tap.on": "Tap opened",
|
|
248
|
+
"act.tap.off": "Tap closed",
|
|
249
|
+
"use.drink": "drink",
|
|
250
|
+
"use.sip": "sip",
|
|
251
|
+
"use.chug": "chug",
|
|
252
|
+
"use.pour": "pour",
|
|
253
|
+
"use.pour_out": "pour out",
|
|
254
|
+
"use.open": "open",
|
|
255
|
+
"use.close": "close",
|
|
256
|
+
"hud.reloading": "Reloading...",
|
|
257
|
+
"hud.empty": "Empty",
|
|
258
|
+
"hud.no_fuel": "Out of fuel",
|
|
259
|
+
"hud.nothing_to_grab": "Nothing to grab here",
|
|
260
|
+
"hud.cannot_grab": "{name} cannot be picked up",
|
|
261
|
+
"hud.too_heavy": "{name} is too heavy ({kg} kg)",
|
|
262
|
+
"hud.empty_container": "{name} is empty",
|
|
263
|
+
"hud.refused": "Wrong tool for the {kind}",
|
|
264
|
+
"hud.drop": "drop",
|
|
265
|
+
"hud.rotate": "rotate",
|
|
266
|
+
"hud.throw": "throw",
|
|
267
|
+
"hud.tilt": "tilt (hold)",
|
|
268
|
+
"hud.equip": "equip {weapon}",
|
|
269
|
+
"ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 Tab: panels \xB7 Esc: free the mouse",
|
|
270
|
+
"ui.log": "Behaviour log",
|
|
271
|
+
"ui.filter.action": "actions",
|
|
272
|
+
"ui.filter.event": "events",
|
|
273
|
+
"ui.filter.check": "checks",
|
|
274
|
+
"ui.autoscroll": "autoscroll",
|
|
275
|
+
"ui.copy_json": "Copy JSON",
|
|
276
|
+
"ui.download_ndjson": "Download NDJSON",
|
|
277
|
+
"ui.download_txt": "Download TXT",
|
|
278
|
+
"ui.clear_log": "Clear",
|
|
279
|
+
"ui.catalog": "Catalogue",
|
|
280
|
+
"ui.clear": "Clear",
|
|
281
|
+
"ui.reset_all": "Reset all",
|
|
282
|
+
"ui.slow": "Slow motion 0.25\xD7",
|
|
283
|
+
"ui.fast": "Time \xD710",
|
|
284
|
+
"ui.tap": "Tap",
|
|
285
|
+
"ui.quality": "Quality",
|
|
286
|
+
"ui.quality.low": "low",
|
|
287
|
+
"ui.quality.medium": "medium",
|
|
288
|
+
"ui.quality.high": "high",
|
|
289
|
+
"ui.equip": "Equip as a tool",
|
|
290
|
+
"ui.objects": "{n} objects",
|
|
291
|
+
"ui.tests": "Tests",
|
|
292
|
+
"ui.run_all": "Run all",
|
|
293
|
+
"ui.running": "Running...",
|
|
294
|
+
"ui.report": "{passed} passed, {failed} failed",
|
|
295
|
+
"ui.panel": "Panels",
|
|
296
|
+
"ui.inspector": "Inspector",
|
|
297
|
+
"ui.aim_at_something": "Aim at an object",
|
|
298
|
+
"ins.material": "Material",
|
|
299
|
+
"ins.status": "Status",
|
|
300
|
+
"ins.burning": "burning",
|
|
301
|
+
"ins.hp": "Hit points",
|
|
302
|
+
"ins.temperature": "Temperature",
|
|
303
|
+
"ins.char": "Charred",
|
|
304
|
+
"ins.melt": "Melted",
|
|
305
|
+
"ins.liters": "Liquid",
|
|
306
|
+
"ins.damage": "Marks",
|
|
307
|
+
"ins.damage_counts": "{dents}, {holes}, {cracks}",
|
|
308
|
+
"ins.mass": "Mass",
|
|
309
|
+
"ins.load": "Load",
|
|
310
|
+
"ins.stress": "Stress",
|
|
311
|
+
"ins.joints": "Joints",
|
|
312
|
+
"ins.open": "open",
|
|
313
|
+
"sc.start": "Scenario: {name}",
|
|
314
|
+
"sc.pass": "Scenario passed: {name}",
|
|
315
|
+
"sc.fail": "Scenario FAILED: {name}",
|
|
316
|
+
"sc.error": "the scenario threw an error: {msg}",
|
|
317
|
+
"leg.still": "leg_1 still attached",
|
|
318
|
+
"leg.gone": "leg_1 came off",
|
|
319
|
+
"sc.steel_through_hole": "9 mm into a steel sheet makes a through hole",
|
|
320
|
+
"sc.steel_through_hole.check": "9 mm into 3 mm steel: {holes} hole(s) of {mm} mm, went through: {through}, sheet {hp} hp (expected: through)",
|
|
321
|
+
"sc.concrete_stops_bullet": "9 mm into a concrete block does not go through",
|
|
322
|
+
"sc.concrete_stops_bullet.check": "9 mm into 40 cm of concrete: went through: {through}, {dents}, {holes}, {cracks}, block {hp} hp (expected: stopped, wall behind untouched)",
|
|
323
|
+
"sc.hammer_dents_drum": "Hammer dents a steel drum",
|
|
324
|
+
"sc.hammer_dents_drum.check": "Hammer on the drum: {swings} swing(s), {dents} dent(s), first {depth} mm deep, body {hp} hp (expected: dented, not broken)",
|
|
325
|
+
"sc.glass_shatters": "Glass pane shatters after a few 9 mm shots",
|
|
326
|
+
"sc.glass_shatters.check": "Glass pane: broken after {shots} shot(s) into {n} shards (expected: broken, at least 4 shards)",
|
|
327
|
+
"sc.shotgun_splinters_crate": "Shotgun splinters a wooden crate",
|
|
328
|
+
"sc.shotgun_splinters_crate.check": "12 ga on the crate: {shots} shot(s), {splinters} board(s) splintered, {holes} holes (expected: at least one splintered)",
|
|
329
|
+
"sc.kick_chair_leg": "A kick takes a chair leg off",
|
|
330
|
+
"sc.kick_chair_leg.check": "Chair: {kicks} kick(s), {groups} group(s), loose pieces {before} \u2192 {after}, {leg} (expected: a loose piece appears)",
|
|
331
|
+
"sc.screwdriver_shelf_board": "Screwdriver takes a screwed shelf board off in about 2.5 s",
|
|
332
|
+
"sc.screwdriver_shelf_board.check": "Screwdriver on the shelf board: took {secs} s, expected about {want} s, the shelf is now in {pieces}",
|
|
333
|
+
"sc.wrong_tool_refused": "The wrong tool is refused",
|
|
334
|
+
"sc.wrong_tool_refused.check": "Screwdriver on a weld: refused {refused} time(s), joints opened {opened} (expected: refused, none opened)",
|
|
335
|
+
"sc.lighter_burns_crate": "A lighter ignites a wooden crate and it ends as ash",
|
|
336
|
+
"sc.lighter_burns_crate.lit": "Lighter on the crate: burning: {lit} (expected: yes)",
|
|
337
|
+
"sc.lighter_burns_crate.ash": "After burning for 500 s: {ash} of {total} boards are ash, most charred {char}% (expected: all ash or charred)",
|
|
338
|
+
"sc.torch_melts_steel": "Wood fire does not melt a steel drum, the cutting torch does",
|
|
339
|
+
"sc.torch_melts_steel.wood": "Burning crate next to the drum: drum peaked at {peak} \xB0C, melted {melt}%, crate still burning: {crate} (expected: no melt)",
|
|
340
|
+
"sc.torch_melts_steel.torch": "Cutting torch on the drum: {temp} \xB0C, melted {melt}% (expected: melting)",
|
|
341
|
+
"sc.bucket_melts_drips": "A plastic bucket melts and drips near fire",
|
|
342
|
+
"sc.bucket_melts_drips.check": "Bucket in flames: melted {melt}%, {temp} \xB0C, {drips} drip line(s) (expected: melting)",
|
|
343
|
+
"sc.gas_bottle_explodes": "A gas bottle in a fire explodes and pushes a crate",
|
|
344
|
+
"sc.gas_bottle_explodes.explode": "Gas bottle in fire: exploded after {seconds} s (expected: explodes)",
|
|
345
|
+
"sc.gas_bottle_explodes.push": "Crate 1.3 m away: moved {moved} m, lost {lost} (expected: pushed or broken)",
|
|
346
|
+
"sc.bottle_drop_shatters": "A glass bottle dropped from 1.5 m shatters",
|
|
347
|
+
"sc.bottle_drop_shatters.check": "Bottle dropped from 1.5 m: status {status}, {cracks} cracks, water stain: {stain} (expected: broken, water on the floor)",
|
|
348
|
+
"sc.throw_chair_at_wall": "A chair thrown at the wall deals blunt damage",
|
|
349
|
+
"sc.throw_chair_at_wall.check": "Chair thrown at the wall: {impacts} impact(s), first {energy} J, {wall} damage line(s) on the wall (expected: impact and damage)",
|
|
350
|
+
"sc.coffee_three_sips": "Three sips of coffee lower the level and log caffeine",
|
|
351
|
+
"sc.coffee_three_sips.check": "Coffee: {sips} sip(s), level {before} \u2192 {after} l, caffeine logged: {caffeine}, burned the mouth: {burn} (expected: 3 sips, caffeine)",
|
|
352
|
+
"sc.tilt_cup_spills": "Tilting the cup spills coffee and leaves a stain",
|
|
353
|
+
"sc.tilt_cup_spills.check": "Cup tilted: level {before} \u2192 {after} l, {stains} coffee stain(s) (expected: spilled with a stain)",
|
|
354
|
+
"sc.jerrycan_leaks_fuel": "Shooting the jerrycan makes a leak and a fuel stain",
|
|
355
|
+
"sc.jerrycan_leaks_fuel.check": "Jerrycan shot: {streams} leak stream(s), fuel stain: {stain}, {left} l left of 20, hole line mentions the leak: {leak} (expected: leaking, stain)",
|
|
356
|
+
"sc.fuel_stain_burns": "A lit fuel stain burns",
|
|
357
|
+
"sc.fuel_stain_burns.stain": "Fuel stain on the floor after the leak: {stain} (expected: yes)",
|
|
358
|
+
"sc.fuel_stain_burns.ignite": "Lighter on the stain: {ignited} ignition(s), fire reached nearby objects: {near} (expected: ignited)",
|
|
359
|
+
"sc.fuel_stain_burns.out": "After 30 s the stain burned out: {burned} (expected: yes)",
|
|
360
|
+
"roof.standing": "roof standing",
|
|
361
|
+
"roof.fell": "roof fell",
|
|
362
|
+
"sc.one_column_roof_stands": "One column destroyed: the roof stays up",
|
|
363
|
+
"sc.one_column_roof_stands.check": "Frame with one column gone: {roof}, heaviest collapse {heavy} kg, peak stress {stress}% (expected: standing, no big collapse)",
|
|
364
|
+
"sc.two_columns_roof_collapses": "Two columns destroyed: the roof collapses",
|
|
365
|
+
"sc.two_columns_roof_collapses.check": "Frame with two columns gone: the others at {stress}% right away, then heavy loose pieces: {pieces} ({kg} kg), collapse event logged: {logged} (expected: the roof comes down)",
|
|
366
|
+
"sc.extinguisher_puts_out_fire": "The extinguisher puts out a burning crate",
|
|
367
|
+
"sc.extinguisher_puts_out_fire.burning": "Crate set on fire: burning: {burning} (expected: yes)",
|
|
368
|
+
"sc.extinguisher_puts_out_fire.out": "After the extinguisher: still burning: {burning}, {events} extinguish event(s) (expected: out)"
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
var ptBR = {
|
|
372
|
+
items: {
|
|
373
|
+
chair: "Cadeira de madeira",
|
|
374
|
+
sofa: "Sof\xE1",
|
|
375
|
+
shelf: "Estante com prateleiras",
|
|
376
|
+
old_tv: "TV antiga",
|
|
377
|
+
steel_sheet: "Chapa de a\xE7o com suporte",
|
|
378
|
+
glass_pane: "Vidro de janela",
|
|
379
|
+
concrete_block: "Bloco de concreto",
|
|
380
|
+
brick_column: "Coluna de tijolos",
|
|
381
|
+
building_frame: "Estrutura de pr\xE9dio",
|
|
382
|
+
car_door: "Porta de carro",
|
|
383
|
+
small_tree: "\xC1rvore pequena",
|
|
384
|
+
potted_plant: "Vaso de planta",
|
|
385
|
+
coffee_cup: "Copo de caf\xE9",
|
|
386
|
+
glass_bottle: "Garrafa de vidro",
|
|
387
|
+
wooden_crate: "Caixote de madeira",
|
|
388
|
+
steel_drum: "Tambor de a\xE7o",
|
|
389
|
+
plastic_bucket: "Balde de pl\xE1stico",
|
|
390
|
+
jerrycan: "Gal\xE3o",
|
|
391
|
+
gas_bottle: "Botij\xE3o de g\xE1s"
|
|
392
|
+
},
|
|
393
|
+
groups: { furniture: "M\xF3veis", structure: "Estrutura", vehicles: "Ve\xEDculos", plants: "Plantas", items: "Itens", containers: "Recipientes" },
|
|
394
|
+
text: {
|
|
395
|
+
"locale.decimal": ",",
|
|
396
|
+
yes: "sim",
|
|
397
|
+
no: "n\xE3o",
|
|
398
|
+
piece: "pe\xE7a",
|
|
399
|
+
coords: "x {x} \xB7 z {z}",
|
|
400
|
+
"count.piece.one": "1 pe\xE7a",
|
|
401
|
+
"count.piece.other": "{n} pe\xE7as",
|
|
402
|
+
"count.joint.one": "1 encaixe",
|
|
403
|
+
"count.joint.other": "{n} encaixes",
|
|
404
|
+
"count.dent.one": "1 amassado",
|
|
405
|
+
"count.dent.other": "{n} amassados",
|
|
406
|
+
"count.hole.one": "1 furo",
|
|
407
|
+
"count.hole.other": "{n} furos",
|
|
408
|
+
"count.crack.one": "1 trinca",
|
|
409
|
+
"count.crack.other": "{n} trincas",
|
|
410
|
+
"joint.generic": "junta",
|
|
411
|
+
"lost.broken": "quebrou",
|
|
412
|
+
"lost.melted": "derreteu",
|
|
413
|
+
"lost.ashes": "virou cinza",
|
|
414
|
+
"tool.none": "s\xF3 com a m\xE3o",
|
|
415
|
+
"arena.target_wall": "Parede de alvo",
|
|
416
|
+
"arena.table": "Mesa",
|
|
417
|
+
"arena.shelf": "Estante",
|
|
418
|
+
"surface.floor": "no ch\xE3o",
|
|
419
|
+
"surface.ground": "no ch\xE3o",
|
|
420
|
+
"surface.table": "na mesa",
|
|
421
|
+
"by.break": "arrebentou",
|
|
422
|
+
"by.tool": "desmontado com ferramenta",
|
|
423
|
+
"by.part_lost": "a pe\xE7a ao lado se perdeu",
|
|
424
|
+
"dmg.ballistic": "de tiro",
|
|
425
|
+
"dmg.blunt": "de impacto",
|
|
426
|
+
"dmg.slash": "de corte",
|
|
427
|
+
"dmg.explosive": "de explos\xE3o",
|
|
428
|
+
"dmg.fire": "de fogo",
|
|
429
|
+
"dmg.heat": "de calor",
|
|
430
|
+
"effect.caffeine": "cafe\xEDna",
|
|
431
|
+
"effect.alcohol": "\xE1lcool",
|
|
432
|
+
"effect.poison": "veneno",
|
|
433
|
+
"effect.disgust": "nojo",
|
|
434
|
+
"ev.dent": "{where}: amassado de {mm} mm, {hp}",
|
|
435
|
+
"ev.hole.through": "{where}: furo de {mm} mm atravessou, {hp}",
|
|
436
|
+
"ev.hole.stuck": "{where}: furo de {mm} mm n\xE3o atravessou, {hp}",
|
|
437
|
+
"ev.dents": "{where}: {n} amassados, o mais fundo com {mm} mm, {hp}",
|
|
438
|
+
"ev.holes": "{where}: {n} furos de {mm} mm, {thr} atravessaram, {hp}",
|
|
439
|
+
"ev.leak.tail": ", vazando {liquid} a {rate} l/s",
|
|
440
|
+
"ev.leak.alone": "{where}: vazando {liquid} a {rate} l/s",
|
|
441
|
+
"ev.crack": "{where}: trincou, {rays} raios de at\xE9 {cm} cm, {hp}",
|
|
442
|
+
"ev.ricochet": "{where}: a bala ricocheteou, sobraram {energy} J",
|
|
443
|
+
"ev.fracture.shatter": "{where}: estilha\xE7ou em {n} cacos",
|
|
444
|
+
"ev.fracture.crumble": "{where}: esfarelou em {n} peda\xE7os",
|
|
445
|
+
"ev.fracture.splinter": "{where}: lascou em {n} peda\xE7os",
|
|
446
|
+
"ev.fracture.tear": "{where}: rasgou em {n} retalhos",
|
|
447
|
+
"ev.fracture.snap": "{where}: partiu em {n} peda\xE7os",
|
|
448
|
+
"ev.fracture.none": "{where}: quebrou em {n} peda\xE7os",
|
|
449
|
+
"ev.detach": "{obj}: {a} soltou de {b} ({by}); o objeto agora tem {pieces}",
|
|
450
|
+
"ev.detach.many.break": "{obj}: {joints} arrebentaram; o objeto agora tem {pieces}",
|
|
451
|
+
"ev.detach.many.tool": "{obj}: {joints} soltos com ferramenta; o objeto agora tem {pieces}",
|
|
452
|
+
"ev.lost.tail": " e se soltou de {joints}; o objeto agora tem {pieces}",
|
|
453
|
+
"ev.lost.alone": "{where}: {verb} e se soltou de {joints}; o objeto agora tem {pieces}",
|
|
454
|
+
"ev.cracks": "{where}: rachando sob o peso, {n} trincas at\xE9 agora, a maior com {cm} cm, {hp}",
|
|
455
|
+
"ev.disassemble": "{obj}: soltando {kind} entre {a} e {b} com {tool}: {pct}%",
|
|
456
|
+
"ev.ignite": "{where}: pegou fogo a {temp} \xB0C",
|
|
457
|
+
"ev.ignite.many": "{obj}: {n} pe\xE7as pegaram fogo a {temp} \xB0C ({parts})",
|
|
458
|
+
"ev.extinguish": "{where}: fogo apagado",
|
|
459
|
+
"ev.extinguish.many": "{obj}: fogo apagado em {n} pe\xE7as ({parts})",
|
|
460
|
+
"ev.burnout": "{where}: virou cinza",
|
|
461
|
+
"ev.melt": "{where}: derretendo, {pct}% j\xE1 derreteu",
|
|
462
|
+
"ev.drip": "{where}: pingando material derretido",
|
|
463
|
+
"ev.explode": "{where}: explodiu, {energy} J em {radius} m",
|
|
464
|
+
"ev.destroyed": "{obj}: destru\xEDdo",
|
|
465
|
+
"ev.damage": "{where}: dano {type} de {amount}, {hp}",
|
|
466
|
+
"ev.damage.overload": "{where}: esmagado pelo peso que sustenta, perdeu {amount} hp at\xE9 agora, {hp}",
|
|
467
|
+
"ev.stress": "{where}: sobrecarregado a {stress}% da capacidade, sustentando {load} kg",
|
|
468
|
+
"ev.collapse": "{obj}: desabou, ca\xEDram {pieces} ({mass} kg): {parts}",
|
|
469
|
+
"ev.stain_ignite": "A mancha de {liquid} em x {x} \xB7 z {z} pegou fogo e vai queimar por uns {seconds} s",
|
|
470
|
+
"ev.stain_burned": "A mancha em chamas se consumiu",
|
|
471
|
+
"ev.fire_dries": "O fogo secou as manchas molhadas por perto",
|
|
472
|
+
"fl.fill": "{who}: encheu de {liquid} (+{liters} l)",
|
|
473
|
+
"fl.pour": "{who}: despejando {liquid} a {rate} l/s",
|
|
474
|
+
"fl.spill": "{who}: derramou {liquid} pela borda a {rate} l/s",
|
|
475
|
+
"fl.leak": "{who}: vazando {liquid} a {rate} l/s",
|
|
476
|
+
"fl.drink": "{who}: bebeu {liters} l, sede +{thirst}",
|
|
477
|
+
"fl.burn": "quente demais: queimou a boca",
|
|
478
|
+
"fl.empty": "{who}: vazio",
|
|
479
|
+
"fl.splash": "Respingo de {liquid}",
|
|
480
|
+
"fl.stain": "Mancha de {liquid} {surface} em {where}",
|
|
481
|
+
"fl.stains": "{n} manchas de {liquid} {surface}, a \xFAltima em {where}",
|
|
482
|
+
"fl.dried": "Uma mancha secou",
|
|
483
|
+
"fl.dried.mark": "Uma mancha secou e deixou marca",
|
|
484
|
+
"fl.dried.known": "A mancha de {liquid} {surface} em {where} secou",
|
|
485
|
+
"fl.dried.known.mark": "A mancha de {liquid} {surface} em {where} secou e deixou marca",
|
|
486
|
+
"fl.dried.many": "{n} manchas de {liquid} secaram",
|
|
487
|
+
"fl.dried.many.mark": "{n} manchas de {liquid} secaram e deixaram marca",
|
|
488
|
+
"fl.mix": "{who}: {from} misturou em {into}",
|
|
489
|
+
"act.anim_start": '{name}: anima\xE7\xE3o "{clip}" ({seconds} s)',
|
|
490
|
+
"act.anim_event": '{name}: "{clip}" evento {type}',
|
|
491
|
+
"act.anim_end": '{name}: anima\xE7\xE3o "{clip}" terminou',
|
|
492
|
+
"act.module_loaded": 'M\xF3dulo "{name}" carregado: {items} item(ns), {scenarios} cen\xE1rio(s), verifica\xE7\xF5es do contrato: {result}',
|
|
493
|
+
"mod.all_ok": "todas passaram",
|
|
494
|
+
"mod.some_failed": "algumas FALHARAM",
|
|
495
|
+
"mod.destructible_ok": "{name}: contrato de destrui\xE7\xE3o ok ({parts} pe\xE7as, golpe e estado bem formados)",
|
|
496
|
+
"mod.destructible_fail": "{name}: contrato de destrui\xE7\xE3o quebrado: {problems}",
|
|
497
|
+
"mod.animation_ok": '{name}: anima\xE7\xE3o "{clip}" ok ({seconds} s)',
|
|
498
|
+
"mod.animation_fail": '{name}: anima\xE7\xE3o "{clip}" quebrada: {problems}',
|
|
499
|
+
"ui.animations": "Anima\xE7\xF5es",
|
|
500
|
+
"ui.animations_aim": "Mire num item ou segure-o para ver as anima\xE7\xF5es.",
|
|
501
|
+
"ui.animations_none": "Este item n\xE3o traz anima\xE7\xF5es.",
|
|
502
|
+
"act.spawn": "Criou {name} em x {x} \xB7 z {z}",
|
|
503
|
+
"act.clear": "Limpou a sala",
|
|
504
|
+
"act.reset": "Consertou todos os objetos",
|
|
505
|
+
"act.tool": "Equipou {weapon}",
|
|
506
|
+
"act.shoot.hit": "{weapon}: disparo, {energy} J, acertou {where} a {dist} m",
|
|
507
|
+
"act.shoot.miss": "{weapon}: disparo, {energy} J, n\xE3o acertou nada",
|
|
508
|
+
"act.shoot.pellets": "{weapon}: disparo, {n} proj\xE9teis de at\xE9 {energy} J, {hits} acertaram {where} a {dist} m",
|
|
509
|
+
"act.melee.hit": "{weapon}: golpe, {energy} J, acertou {where} a {dist} m",
|
|
510
|
+
"act.melee.miss": "{weapon}: golpe no ar",
|
|
511
|
+
"act.flame.hit": "{weapon}: chama a {temp} \xB0C em {where}",
|
|
512
|
+
"act.flame.air": "{weapon}: chama, nada ao alcance",
|
|
513
|
+
"act.heat.hit": "{weapon}: esquentando {where} a {temp} \xB0C",
|
|
514
|
+
"act.heat.air": "{weapon}: nada ao alcance",
|
|
515
|
+
"act.throw.grenade": "{weapon}: arremessada, pavio de {fuse} s",
|
|
516
|
+
"act.throw.molotov": "{weapon}: arremessado, quebra no impacto",
|
|
517
|
+
"act.explode": "{weapon}: explodiu, {energy} J em {radius} m, {n} objetos ao alcance",
|
|
518
|
+
"act.molotov_fire": "{weapon}: fogo no ch\xE3o, raio de {radius} m por {seconds} s",
|
|
519
|
+
"act.tool.none": "{weapon}: nada ao alcance",
|
|
520
|
+
"act.tool.nojoint": "{weapon}: nenhuma junta perto de {where}",
|
|
521
|
+
"act.tool.refused": "{weapon} n\xE3o solta {kind} de {obj}; precisa de: {need}",
|
|
522
|
+
"act.extinguish.hit": "{weapon}: espuma em {what}",
|
|
523
|
+
"act.extinguish.air": "{weapon}: espuma, nada ao alcance",
|
|
524
|
+
"act.grab": "Pegou {name} ({kg} kg)",
|
|
525
|
+
"act.drop": "Soltou {name}",
|
|
526
|
+
"act.throw": "Arremessou {name} a {speed} m/s",
|
|
527
|
+
"act.landing": "{name} caiu a {speed} m/s: {energy} J",
|
|
528
|
+
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
529
|
+
"act.use_item": "{name}: {use}",
|
|
530
|
+
"act.tap.on": "Abriu a torneira",
|
|
531
|
+
"act.tap.off": "Fechou a torneira",
|
|
532
|
+
"use.drink": "beber",
|
|
533
|
+
"use.sip": "golinho",
|
|
534
|
+
"use.chug": "virar",
|
|
535
|
+
"use.pour": "despejar",
|
|
536
|
+
"use.pour_out": "esvaziar",
|
|
537
|
+
"use.open": "abrir",
|
|
538
|
+
"use.close": "fechar",
|
|
539
|
+
"hud.reloading": "Recarregando...",
|
|
540
|
+
"hud.empty": "Sem muni\xE7\xE3o",
|
|
541
|
+
"hud.no_fuel": "Sem combust\xEDvel",
|
|
542
|
+
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
543
|
+
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
544
|
+
"hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
|
|
545
|
+
"hud.empty_container": "{name} est\xE1 vazio",
|
|
546
|
+
"hud.refused": "Ferramenta errada para {kind}",
|
|
547
|
+
"hud.drop": "soltar",
|
|
548
|
+
"hud.rotate": "girar",
|
|
549
|
+
"hud.throw": "arremessar",
|
|
550
|
+
"hud.tilt": "inclinar (segurar)",
|
|
551
|
+
"hud.equip": "equipar {weapon}",
|
|
552
|
+
"ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
|
|
553
|
+
"ui.log": "Registro de comportamento",
|
|
554
|
+
"ui.filter.action": "a\xE7\xF5es",
|
|
555
|
+
"ui.filter.event": "eventos",
|
|
556
|
+
"ui.filter.check": "testes",
|
|
557
|
+
"ui.autoscroll": "rolar sozinho",
|
|
558
|
+
"ui.copy_json": "Copiar JSON",
|
|
559
|
+
"ui.download_ndjson": "Baixar NDJSON",
|
|
560
|
+
"ui.download_txt": "Baixar TXT",
|
|
561
|
+
"ui.clear_log": "Limpar",
|
|
562
|
+
"ui.catalog": "Cat\xE1logo",
|
|
563
|
+
"ui.clear": "Limpar",
|
|
564
|
+
"ui.reset_all": "Consertar tudo",
|
|
565
|
+
"ui.slow": "C\xE2mera lenta 0,25\xD7",
|
|
566
|
+
"ui.fast": "Tempo \xD710",
|
|
567
|
+
"ui.tap": "Torneira",
|
|
568
|
+
"ui.quality": "Qualidade",
|
|
569
|
+
"ui.quality.low": "baixa",
|
|
570
|
+
"ui.quality.medium": "m\xE9dia",
|
|
571
|
+
"ui.quality.high": "alta",
|
|
572
|
+
"ui.equip": "Equipar como ferramenta",
|
|
573
|
+
"ui.objects": "{n} objetos",
|
|
574
|
+
"ui.tests": "Testes",
|
|
575
|
+
"ui.run_all": "Rodar todos",
|
|
576
|
+
"ui.running": "Rodando...",
|
|
577
|
+
"ui.report": "{passed} passaram, {failed} falharam",
|
|
578
|
+
"ui.panel": "Pain\xE9is",
|
|
579
|
+
"ui.inspector": "Inspetor",
|
|
580
|
+
"ui.aim_at_something": "Mire em um objeto",
|
|
581
|
+
"ins.material": "Material",
|
|
582
|
+
"ins.status": "Estado",
|
|
583
|
+
"ins.burning": "em chamas",
|
|
584
|
+
"ins.hp": "Pontos de vida",
|
|
585
|
+
"ins.temperature": "Temperatura",
|
|
586
|
+
"ins.char": "Carbonizado",
|
|
587
|
+
"ins.melt": "Derretido",
|
|
588
|
+
"ins.liters": "L\xEDquido",
|
|
589
|
+
"ins.damage": "Marcas",
|
|
590
|
+
"ins.damage_counts": "{dents}, {holes}, {cracks}",
|
|
591
|
+
"ins.mass": "Massa",
|
|
592
|
+
"ins.load": "Carga",
|
|
593
|
+
"ins.stress": "Tens\xE3o",
|
|
594
|
+
"ins.joints": "Juntas",
|
|
595
|
+
"ins.open": "aberta",
|
|
596
|
+
"sc.start": "Cen\xE1rio: {name}",
|
|
597
|
+
"sc.pass": "Cen\xE1rio passou: {name}",
|
|
598
|
+
"sc.fail": "Cen\xE1rio FALHOU: {name}",
|
|
599
|
+
"sc.error": "o cen\xE1rio lan\xE7ou um erro: {msg}",
|
|
600
|
+
"leg.still": "leg_1 continua presa",
|
|
601
|
+
"leg.gone": "leg_1 soltou",
|
|
602
|
+
"sc.steel_through_hole": "Tiro de 9 mm em chapa de a\xE7o faz furo que atravessa",
|
|
603
|
+
"sc.steel_through_hole.check": "9 mm em a\xE7o de 3 mm: {holes} furo(s) de {mm} mm, atravessou: {through}, chapa com {hp} hp (esperado: atravessar)",
|
|
604
|
+
"sc.concrete_stops_bullet": "Tiro de 9 mm em bloco de concreto n\xE3o atravessa",
|
|
605
|
+
"sc.concrete_stops_bullet.check": "9 mm em 40 cm de concreto: atravessou: {through}, {dents}, {holes}, {cracks}, bloco com {hp} hp (esperado: parou, parede atr\xE1s intacta)",
|
|
606
|
+
"sc.hammer_dents_drum": "Martelo amassa um tambor de a\xE7o",
|
|
607
|
+
"sc.hammer_dents_drum.check": "Martelo no tambor: {swings} golpe(s), {dents} amassado(s), o primeiro com {depth} mm, corpo com {hp} hp (esperado: amassado, sem quebrar)",
|
|
608
|
+
"sc.glass_shatters": "Vidro estilha\xE7a depois de alguns tiros de 9 mm",
|
|
609
|
+
"sc.glass_shatters.check": "Vidro: quebrou depois de {shots} tiro(s) em {n} cacos (esperado: quebrado, pelo menos 4 cacos)",
|
|
610
|
+
"sc.shotgun_splinters_crate": "Escopeta lasca um caixote de madeira",
|
|
611
|
+
"sc.shotgun_splinters_crate.check": "Calibre 12 no caixote: {shots} tiro(s), {splinters} t\xE1bua(s) lascada(s), {holes} furos (esperado: pelo menos uma lascada)",
|
|
612
|
+
"sc.kick_chair_leg": "Um chute solta a perna da cadeira",
|
|
613
|
+
"sc.kick_chair_leg.check": "Cadeira: {kicks} chute(s), {groups} grupo(s), pe\xE7as soltas {before} \u2192 {after}, {leg} (esperado: aparece uma pe\xE7a solta)",
|
|
614
|
+
"sc.screwdriver_shelf_board": "Chave de fenda solta a prateleira parafusada em cerca de 2,5 s",
|
|
615
|
+
"sc.screwdriver_shelf_board.check": "Chave de fenda na prateleira: levou {secs} s, esperado cerca de {want} s, a estante agora est\xE1 em {pieces}",
|
|
616
|
+
"sc.wrong_tool_refused": "A ferramenta errada \xE9 recusada",
|
|
617
|
+
"sc.wrong_tool_refused.check": "Chave de fenda numa solda: recusou {refused} vez(es), juntas abertas {opened} (esperado: recusar, nenhuma aberta)",
|
|
618
|
+
"sc.lighter_burns_crate": "Isqueiro acende o caixote de madeira e ele vira cinza",
|
|
619
|
+
"sc.lighter_burns_crate.lit": "Isqueiro no caixote: pegou fogo: {lit} (esperado: sim)",
|
|
620
|
+
"sc.lighter_burns_crate.ash": "Depois de queimar por 500 s: {ash} de {total} t\xE1buas viraram cinza, a mais queimada {char}% (esperado: tudo cinza ou carbonizado)",
|
|
621
|
+
"sc.torch_melts_steel": "Fogo de madeira n\xE3o derrete o tambor de a\xE7o, o ma\xE7arico derrete",
|
|
622
|
+
"sc.torch_melts_steel.wood": "Caixote em chamas ao lado do tambor: o tambor chegou a {peak} \xB0C, derreteu {melt}%, caixote ainda queimando: {crate} (esperado: sem derreter)",
|
|
623
|
+
"sc.torch_melts_steel.torch": "Ma\xE7arico no tambor: {temp} \xB0C, derreteu {melt}% (esperado: derretendo)",
|
|
624
|
+
"sc.bucket_melts_drips": "Balde de pl\xE1stico derrete e pinga perto do fogo",
|
|
625
|
+
"sc.bucket_melts_drips.check": "Balde nas chamas: derreteu {melt}%, {temp} \xB0C, {drips} linha(s) de pingos (esperado: derretendo)",
|
|
626
|
+
"sc.gas_bottle_explodes": "Botij\xE3o de g\xE1s no fogo explode e empurra um caixote",
|
|
627
|
+
"sc.gas_bottle_explodes.explode": "Botij\xE3o no fogo: explodiu depois de {seconds} s (esperado: explode)",
|
|
628
|
+
"sc.gas_bottle_explodes.push": "Caixote a 1,3 m: andou {moved} m, perdeu {lost} (esperado: empurrado ou quebrado)",
|
|
629
|
+
"sc.bottle_drop_shatters": "Garrafa de vidro solta de 1,5 m estilha\xE7a",
|
|
630
|
+
"sc.bottle_drop_shatters.check": "Garrafa solta de 1,5 m: estado {status}, {cracks} trincas, mancha de \xE1gua: {stain} (esperado: quebrada, \xE1gua no ch\xE3o)",
|
|
631
|
+
"sc.throw_chair_at_wall": "Cadeira arremessada na parede causa dano de impacto",
|
|
632
|
+
"sc.throw_chair_at_wall.check": "Cadeira na parede: {impacts} impacto(s), o primeiro com {energy} J, {wall} linha(s) de dano na parede (esperado: impacto e dano)",
|
|
633
|
+
"sc.coffee_three_sips": "Tr\xEAs goles de caf\xE9 baixam o n\xEDvel e registram cafe\xEDna",
|
|
634
|
+
"sc.coffee_three_sips.check": "Caf\xE9: {sips} gole(s), n\xEDvel {before} \u2192 {after} l, cafe\xEDna registrada: {caffeine}, queimou a boca: {burn} (esperado: 3 goles, cafe\xEDna)",
|
|
635
|
+
"sc.tilt_cup_spills": "Inclinar o copo derrama caf\xE9 e deixa mancha",
|
|
636
|
+
"sc.tilt_cup_spills.check": "Copo inclinado: n\xEDvel {before} \u2192 {after} l, {stains} mancha(s) de caf\xE9 (esperado: derramou com mancha)",
|
|
637
|
+
"sc.jerrycan_leaks_fuel": "Tiro no gal\xE3o faz vazamento e mancha de combust\xEDvel",
|
|
638
|
+
"sc.jerrycan_leaks_fuel.check": "Gal\xE3o baleado: {streams} jato(s) de vazamento, mancha de combust\xEDvel: {stain}, restam {left} l de 20, a linha do furo cita o vazamento: {leak} (esperado: vazando, mancha)",
|
|
639
|
+
"sc.fuel_stain_burns": "Mancha de combust\xEDvel acesa queima",
|
|
640
|
+
"sc.fuel_stain_burns.stain": "Mancha de combust\xEDvel no ch\xE3o depois do vazamento: {stain} (esperado: sim)",
|
|
641
|
+
"sc.fuel_stain_burns.ignite": "Isqueiro na mancha: {ignited} igni\xE7\xE3o(\xF5es), o fogo alcan\xE7ou objetos por perto: {near} (esperado: acendeu)",
|
|
642
|
+
"sc.fuel_stain_burns.out": "Depois de 30 s a mancha se consumiu: {burned} (esperado: sim)",
|
|
643
|
+
"roof.standing": "laje de p\xE9",
|
|
644
|
+
"roof.fell": "laje caiu",
|
|
645
|
+
"sc.one_column_roof_stands": "Pilar destru\xEDdo: a laje fica de p\xE9",
|
|
646
|
+
"sc.one_column_roof_stands.check": "Estrutura sem um pilar: {roof}, maior desabamento {heavy} kg, tens\xE3o m\xE1xima {stress}% (esperado: de p\xE9, sem desabamento grande)",
|
|
647
|
+
"sc.two_columns_roof_collapses": "Dois pilares destru\xEDdos: a laje desaba",
|
|
648
|
+
"sc.two_columns_roof_collapses.check": "Estrutura sem dois pilares: os outros logo a {stress}%, depois pe\xE7as pesadas soltas: {pieces} ({kg} kg), evento de desabamento registrado: {logged} (esperado: a laje desaba)",
|
|
649
|
+
"sc.extinguisher_puts_out_fire": "Extintor apaga um caixote em chamas",
|
|
650
|
+
"sc.extinguisher_puts_out_fire.burning": "Caixote incendiado: queimando: {burning} (esperado: sim)",
|
|
651
|
+
"sc.extinguisher_puts_out_fire.out": "Depois do extintor: ainda queimando: {burning}, {events} evento(s) de apagar (esperado: apagado)"
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
var locales = { en, "pt-BR": ptBR };
|
|
655
|
+
var resolveStrings = (locale) => locales[locale ?? "en"] ?? locales.en;
|
|
656
|
+
|
|
657
|
+
// src/items.ts
|
|
658
|
+
var DEMO_MATERIALS = [];
|
|
659
|
+
var box = (id, material, size, position, extra = {}) => ({ id, material, shape: { kind: "box", size }, position, ...extra });
|
|
660
|
+
var cyl = (id, material, radius, height, position, extra = {}) => ({
|
|
661
|
+
id,
|
|
662
|
+
material,
|
|
663
|
+
shape: { kind: "cylinder", radius, height },
|
|
664
|
+
position,
|
|
665
|
+
...extra
|
|
666
|
+
});
|
|
667
|
+
var sph = (id, material, radius, position, extra = {}) => ({ id, material, shape: { kind: "sphere", radius }, position, ...extra });
|
|
668
|
+
var joint = (a, b, kind, extra = {}) => ({ a, b, kind, ...extra });
|
|
669
|
+
function brickWallSpec(cols = 8, rows = 6, panel = [1.25, 0.5, 0.2]) {
|
|
670
|
+
const parts = [];
|
|
671
|
+
const joints = [];
|
|
672
|
+
const id = (c, r) => `brick_${r}_${c}`;
|
|
673
|
+
for (let r = 0; r < rows; r++) {
|
|
674
|
+
for (let c = 0; c < cols; c++) {
|
|
675
|
+
parts.push(box(id(c, r), "brick", panel, [(c - (cols - 1) / 2) * panel[0], panel[1] / 2 + r * panel[1], 0], { anchored: r === 0 }));
|
|
676
|
+
if (c > 0) joints.push(joint(id(c - 1, r), id(c, r), "mortar"));
|
|
677
|
+
if (r > 0) joints.push(joint(id(c, r - 1), id(c, r), "mortar"));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { name: "brick wall", parts, joints, tags: ["structure"] };
|
|
681
|
+
}
|
|
682
|
+
var chair = () => {
|
|
683
|
+
const legs = [
|
|
684
|
+
[-0.18, -0.18],
|
|
685
|
+
[0.18, -0.18],
|
|
686
|
+
[-0.18, 0.18],
|
|
687
|
+
[0.18, 0.18]
|
|
688
|
+
];
|
|
689
|
+
return {
|
|
690
|
+
name: "chair",
|
|
691
|
+
parts: [
|
|
692
|
+
box("seat", "wood_soft", [0.42, 0.04, 0.42], [0, 0.45, 0]),
|
|
693
|
+
...legs.map(([x, z], i) => box(`leg_${i + 1}`, "wood_soft", [0.04, 0.43, 0.04], [x, 0.215, z])),
|
|
694
|
+
box("backrest", "wood_soft", [0.4, 0.3, 0.03], [0, 0.64, -0.195])
|
|
695
|
+
],
|
|
696
|
+
joints: [...legs.map((_, i) => joint(`leg_${i + 1}`, "seat", "screw")), joint("backrest", "seat", "screw")],
|
|
697
|
+
tags: ["furniture"]
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
var crate = () => ({
|
|
701
|
+
name: "wooden crate",
|
|
702
|
+
parts: [
|
|
703
|
+
box("front", "wood_soft", [0.5, 0.5, 0.02], [0, 0.25, 0.24], { hp: 70 }),
|
|
704
|
+
box("back", "wood_soft", [0.5, 0.5, 0.02], [0, 0.25, -0.24], { hp: 70 }),
|
|
705
|
+
box("left", "wood_soft", [0.02, 0.5, 0.46], [-0.24, 0.25, 0], { hp: 70 }),
|
|
706
|
+
box("right", "wood_soft", [0.02, 0.5, 0.46], [0.24, 0.25, 0], { hp: 70 }),
|
|
707
|
+
box("bottom", "wood_soft", [0.46, 0.02, 0.46], [0, 0.01, 0], { hp: 70 })
|
|
708
|
+
],
|
|
709
|
+
joints: [
|
|
710
|
+
joint("front", "left", "nail"),
|
|
711
|
+
joint("front", "right", "nail"),
|
|
712
|
+
joint("back", "left", "nail"),
|
|
713
|
+
joint("back", "right", "nail"),
|
|
714
|
+
joint("front", "bottom", "nail"),
|
|
715
|
+
joint("back", "bottom", "nail"),
|
|
716
|
+
joint("left", "bottom", "nail"),
|
|
717
|
+
joint("right", "bottom", "nail")
|
|
718
|
+
],
|
|
719
|
+
tags: ["container"]
|
|
720
|
+
});
|
|
721
|
+
var drum = () => ({
|
|
722
|
+
name: "steel drum",
|
|
723
|
+
parts: [
|
|
724
|
+
cyl("drum_body", "steel", 0.29, 0.86, [0, 0.45, 0], { mass: 12, thickness: 12e-4 }),
|
|
725
|
+
cyl("drum_top", "steel", 0.29, 0.02, [0, 0.89, 0], { mass: 1.5, thickness: 12e-4 }),
|
|
726
|
+
cyl("drum_bottom", "steel", 0.29, 0.02, [0, 0.01, 0], { mass: 1.5, thickness: 12e-4 })
|
|
727
|
+
],
|
|
728
|
+
joints: [joint("drum_body", "drum_top", "weld"), joint("drum_body", "drum_bottom", "weld")],
|
|
729
|
+
tags: ["container"]
|
|
730
|
+
});
|
|
731
|
+
var steelSheet = () => ({
|
|
732
|
+
name: "steel sheet",
|
|
733
|
+
parts: [
|
|
734
|
+
box("sheet", "steel", [1, 1, 3e-3], [0, 1.1, 0]),
|
|
735
|
+
box("post_l", "steel", [0.04, 0.54, 0.04], [-0.35, 0.33, 0], { mass: 4 }),
|
|
736
|
+
box("post_r", "steel", [0.04, 0.54, 0.04], [0.35, 0.33, 0], { mass: 4 }),
|
|
737
|
+
box("foot_l", "steel", [0.06, 0.06, 0.5], [-0.35, 0.03, 0], { mass: 6 }),
|
|
738
|
+
box("foot_r", "steel", [0.06, 0.06, 0.5], [0.35, 0.03, 0], { mass: 6 })
|
|
739
|
+
],
|
|
740
|
+
joints: [joint("sheet", "post_l", "weld"), joint("sheet", "post_r", "weld"), joint("post_l", "foot_l", "weld"), joint("post_r", "foot_r", "weld")],
|
|
741
|
+
tags: ["structure"]
|
|
742
|
+
});
|
|
743
|
+
var glassPane = () => ({
|
|
744
|
+
name: "glass pane",
|
|
745
|
+
parts: [
|
|
746
|
+
box("pane", "glass", [0.8, 1, 6e-3], [0, 1.05, 0], { hp: 40 }),
|
|
747
|
+
box("frame_top", "aluminium", [0.88, 0.04, 0.04], [0, 1.57, 0], { mass: 1 }),
|
|
748
|
+
box("frame_bottom", "aluminium", [0.88, 0.04, 0.04], [0, 0.53, 0], { mass: 1 }),
|
|
749
|
+
box("frame_l", "aluminium", [0.04, 1.04, 0.04], [-0.42, 1.05, 0], { mass: 1 }),
|
|
750
|
+
box("frame_r", "aluminium", [0.04, 1.04, 0.04], [0.42, 1.05, 0], { mass: 1 }),
|
|
751
|
+
box("foot_l", "steel", [0.06, 0.5, 0.4], [-0.35, 0.25, 0], { mass: 5 }),
|
|
752
|
+
box("foot_r", "steel", [0.06, 0.5, 0.4], [0.35, 0.25, 0], { mass: 5 })
|
|
753
|
+
],
|
|
754
|
+
joints: [
|
|
755
|
+
joint("pane", "frame_top", "clip"),
|
|
756
|
+
joint("pane", "frame_bottom", "clip"),
|
|
757
|
+
joint("pane", "frame_l", "clip"),
|
|
758
|
+
joint("pane", "frame_r", "clip"),
|
|
759
|
+
joint("frame_bottom", "foot_l", "bolt"),
|
|
760
|
+
joint("frame_bottom", "foot_r", "bolt"),
|
|
761
|
+
joint("frame_l", "frame_top", "weld"),
|
|
762
|
+
joint("frame_r", "frame_top", "weld"),
|
|
763
|
+
joint("frame_l", "frame_bottom", "weld"),
|
|
764
|
+
joint("frame_r", "frame_bottom", "weld")
|
|
765
|
+
],
|
|
766
|
+
tags: ["structure"]
|
|
767
|
+
});
|
|
768
|
+
var concreteBlock = () => ({
|
|
769
|
+
name: "concrete block",
|
|
770
|
+
parts: [box("block", "concrete", [0.4, 0.4, 0.4], [0, 0.2, 0])],
|
|
771
|
+
tags: ["structure"]
|
|
772
|
+
});
|
|
773
|
+
var brickColumn = () => {
|
|
774
|
+
const parts = [];
|
|
775
|
+
const joints = [];
|
|
776
|
+
const layers = 8;
|
|
777
|
+
const id = (l, x, z) => `brick_${l}_${x}${z}`;
|
|
778
|
+
for (let l = 0; l < layers; l++) {
|
|
779
|
+
for (let x = 0; x < 2; x++) {
|
|
780
|
+
for (let z = 0; z < 2; z++) {
|
|
781
|
+
parts.push(box(id(l, x, z), "brick", [0.2, 0.2, 0.2], [(x - 0.5) * 0.2, 0.1 + l * 0.2, (z - 0.5) * 0.2], { anchored: l === 0 }));
|
|
782
|
+
if (x > 0) joints.push(joint(id(l, x - 1, z), id(l, x, z), "mortar"));
|
|
783
|
+
if (z > 0) joints.push(joint(id(l, x, z - 1), id(l, x, z), "mortar"));
|
|
784
|
+
if (l > 0) joints.push(joint(id(l - 1, x, z), id(l, x, z), "mortar"));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return { name: "brick column", parts, joints, tags: ["structure"] };
|
|
789
|
+
};
|
|
790
|
+
var frame = () => ({ ...buildFrame({ spanX: 4, spanZ: 4, columnsX: 2, columnsZ: 2, height: 2.8, floors: 1, walls: false, roof: true, columnCapacity: 4400 }), name: "building frame" });
|
|
791
|
+
var bucket = () => ({
|
|
792
|
+
name: "plastic bucket",
|
|
793
|
+
parts: [
|
|
794
|
+
cyl("bucket_body", "plastic", 0.14, 0.28, [0, 0.14, 0], { mass: 0.5, thickness: 2e-3 }),
|
|
795
|
+
box("handle", "plastic", [0.3, 0.012, 0.012], [0, 0.3, 0], { mass: 0.05 })
|
|
796
|
+
],
|
|
797
|
+
joints: [joint("handle", "bucket_body", "clip")],
|
|
798
|
+
tags: ["container"]
|
|
799
|
+
});
|
|
800
|
+
var bottle = () => ({
|
|
801
|
+
name: "glass bottle",
|
|
802
|
+
parts: [
|
|
803
|
+
cyl("bottle_body", "glass", 0.037, 0.17, [0, 0.085, 0], { mass: 0.3, thickness: 3e-3, contains: { fluid: "water", liters: 0.5 } }),
|
|
804
|
+
cyl("bottle_neck", "glass", 0.015, 0.09, [0, 0.215, 0], { mass: 0.08, thickness: 2e-3 })
|
|
805
|
+
],
|
|
806
|
+
joints: [joint("bottle_neck", "bottle_body", "fused")],
|
|
807
|
+
tags: ["item"]
|
|
808
|
+
});
|
|
809
|
+
var cup = () => ({
|
|
810
|
+
name: "coffee cup",
|
|
811
|
+
parts: [
|
|
812
|
+
cyl("cup", "ceramic", 0.05, 0.11, [0, 0.055, 0], { mass: 0.3, thickness: 4e-3, contains: { fluid: "coffee", liters: 0.25 } }),
|
|
813
|
+
box("cup_handle", "ceramic", [0.02, 0.06, 0.014], [0.068, 0.06, 0], { mass: 0.04 })
|
|
814
|
+
],
|
|
815
|
+
joints: [joint("cup_handle", "cup", "fused")],
|
|
816
|
+
tags: ["item"]
|
|
817
|
+
});
|
|
818
|
+
var jerrycan = () => ({
|
|
819
|
+
name: "jerrycan",
|
|
820
|
+
parts: [
|
|
821
|
+
box("tank", "plastic", [0.34, 0.45, 0.14], [0, 0.225, 0], { mass: 1.2, thickness: 3e-3, contains: { fluid: "fuel", liters: 20 } }),
|
|
822
|
+
cyl("cap", "plastic", 0.025, 0.03, [0.08, 0.465, 0], { mass: 0.05 }),
|
|
823
|
+
box("handle", "plastic", [0.1, 0.03, 0.03], [-0.07, 0.465, 0], { mass: 0.1 })
|
|
824
|
+
],
|
|
825
|
+
joints: [joint("cap", "tank", "screw"), joint("handle", "tank", "fused")],
|
|
826
|
+
tags: ["container"]
|
|
827
|
+
});
|
|
828
|
+
var gasBottle = () => ({
|
|
829
|
+
name: "gas bottle",
|
|
830
|
+
parts: [
|
|
831
|
+
cyl("cylinder", "steel", 0.15, 0.7, [0, 0.4, 0], {
|
|
832
|
+
mass: 11,
|
|
833
|
+
thickness: 3e-3,
|
|
834
|
+
contains: { fluid: "gas", liters: 13, pressurized: true },
|
|
835
|
+
explodes: { energy: 12e4, radius: 5, atC: 300 }
|
|
836
|
+
}),
|
|
837
|
+
cyl("valve", "steel", 0.03, 0.1, [0, 0.8, 0], { mass: 0.5 }),
|
|
838
|
+
cyl("foot_ring", "steel", 0.14, 0.05, [0, 0.025, 0], { mass: 1 })
|
|
839
|
+
],
|
|
840
|
+
joints: [joint("valve", "cylinder", "weld"), joint("foot_ring", "cylinder", "weld")],
|
|
841
|
+
tags: ["container"]
|
|
842
|
+
});
|
|
843
|
+
var tree = () => ({
|
|
844
|
+
name: "small tree",
|
|
845
|
+
parts: [
|
|
846
|
+
cyl("trunk", "green_wood", 0.08, 2.2, [0, 1.1, 0], { anchored: true }),
|
|
847
|
+
cyl("branch_1", "green_wood", 0.03, 0.8, [0.34, 1.7, 0], { rotation: [0, 0, -0.9] }),
|
|
848
|
+
cyl("branch_2", "green_wood", 0.03, 0.8, [-0.3, 1.9, 0.12], { rotation: [0, 0, 0.8] }),
|
|
849
|
+
cyl("branch_3", "green_wood", 0.025, 0.7, [0.05, 2, -0.28], { rotation: [0.8, 0, 0] }),
|
|
850
|
+
sph("crown_top", "foliage", 0.55, [0, 2.55, 0], { mass: 2 }),
|
|
851
|
+
sph("crown_1", "foliage", 0.38, [0.62, 2, 0], { mass: 1 }),
|
|
852
|
+
sph("crown_2", "foliage", 0.38, [-0.58, 2.2, 0.15], { mass: 1 }),
|
|
853
|
+
sph("crown_3", "foliage", 0.32, [0.05, 2.3, -0.55], { mass: 1 })
|
|
854
|
+
],
|
|
855
|
+
joints: [
|
|
856
|
+
joint("branch_1", "trunk", "stem"),
|
|
857
|
+
joint("branch_2", "trunk", "stem"),
|
|
858
|
+
joint("branch_3", "trunk", "stem"),
|
|
859
|
+
joint("crown_top", "trunk", "stem"),
|
|
860
|
+
joint("crown_1", "branch_1", "stem"),
|
|
861
|
+
joint("crown_2", "branch_2", "stem"),
|
|
862
|
+
joint("crown_3", "branch_3", "stem")
|
|
863
|
+
],
|
|
864
|
+
tags: ["plant"]
|
|
865
|
+
});
|
|
866
|
+
var pottedPlant = () => ({
|
|
867
|
+
name: "potted plant",
|
|
868
|
+
parts: [
|
|
869
|
+
cyl("pot", "ceramic", 0.15, 0.25, [0, 0.125, 0], { mass: 2, thickness: 8e-3 }),
|
|
870
|
+
cyl("soil", "soil", 0.14, 0.06, [0, 0.26, 0], { mass: 1.5 }),
|
|
871
|
+
sph("leaves_1", "foliage", 0.16, [0, 0.46, 0], { mass: 0.3 }),
|
|
872
|
+
sph("leaves_2", "foliage", 0.12, [0.12, 0.38, 0.04], { mass: 0.2 }),
|
|
873
|
+
sph("leaves_3", "foliage", 0.12, [-0.1, 0.4, -0.06], { mass: 0.2 })
|
|
874
|
+
],
|
|
875
|
+
joints: [joint("soil", "pot", "fused"), joint("leaves_1", "soil", "stem"), joint("leaves_2", "soil", "stem"), joint("leaves_3", "soil", "stem")],
|
|
876
|
+
tags: ["plant"]
|
|
877
|
+
});
|
|
878
|
+
var oldTv = () => ({
|
|
879
|
+
name: "old TV",
|
|
880
|
+
parts: [
|
|
881
|
+
box("stand", "wood_soft", [0.6, 0.35, 0.42], [0, 0.175, 0]),
|
|
882
|
+
box("shell", "plastic", [0.58, 0.44, 0.42], [0, 0.57, 0], { mass: 3, thickness: 4e-3 }),
|
|
883
|
+
box("screen", "glass", [0.42, 0.32, 0.012], [0, 0.57, 0.212], { mass: 1.5 }),
|
|
884
|
+
box("coil", "copper", [0.2, 0.1, 0.2], [0, 0.55, -0.05], { mass: 1.5 })
|
|
885
|
+
],
|
|
886
|
+
joints: [joint("shell", "stand", "screw"), joint("screen", "shell", "clip"), joint("coil", "shell", "screw")],
|
|
887
|
+
tags: ["furniture"]
|
|
888
|
+
});
|
|
889
|
+
var carDoor = () => ({
|
|
890
|
+
name: "car door",
|
|
891
|
+
parts: [
|
|
892
|
+
box("post", "steel", [0.1, 1.3, 0.1], [-0.58, 0.65, 0], { mass: 12, anchored: true }),
|
|
893
|
+
box("skin", "steel", [1, 0.8, 0.05], [0, 0.75, 0], { mass: 14, thickness: 8e-4 }),
|
|
894
|
+
box("window", "glass", [0.78, 0.32, 0.01], [0.04, 1.31, 0.01], { mass: 3 }),
|
|
895
|
+
box("seal", "rubber", [0.84, 0.03, 0.03], [0.04, 1.14, 0.01], { mass: 0.3 })
|
|
896
|
+
],
|
|
897
|
+
joints: [
|
|
898
|
+
joint("skin", "post", "hinge", { id: "hinge_upper" }),
|
|
899
|
+
joint("skin", "post", "hinge", { id: "hinge_lower" }),
|
|
900
|
+
joint("window", "seal", "clip"),
|
|
901
|
+
joint("seal", "skin", "clip")
|
|
902
|
+
],
|
|
903
|
+
tags: ["vehicle"]
|
|
904
|
+
});
|
|
905
|
+
var sofa = () => ({
|
|
906
|
+
name: "sofa",
|
|
907
|
+
parts: [
|
|
908
|
+
box("frame", "wood_soft", [1.8, 0.3, 0.8], [0, 0.15, 0], { mass: 14 }),
|
|
909
|
+
box("seat_cushion", "foam", [1.5, 0.18, 0.62], [0, 0.39, 0.06], { mass: 4 }),
|
|
910
|
+
box("back_cushion", "foam", [1.5, 0.5, 0.16], [0, 0.65, -0.3], { mass: 4 }),
|
|
911
|
+
box("arm_l", "fabric", [0.15, 0.4, 0.8], [-0.82, 0.5, 0], { mass: 3 }),
|
|
912
|
+
box("arm_r", "fabric", [0.15, 0.4, 0.8], [0.82, 0.5, 0], { mass: 3 })
|
|
913
|
+
],
|
|
914
|
+
joints: [
|
|
915
|
+
joint("seat_cushion", "frame", "clip"),
|
|
916
|
+
joint("back_cushion", "frame", "clip"),
|
|
917
|
+
joint("arm_l", "frame", "screw"),
|
|
918
|
+
joint("arm_r", "frame", "screw")
|
|
919
|
+
],
|
|
920
|
+
tags: ["furniture"]
|
|
921
|
+
});
|
|
922
|
+
var shelf = () => ({
|
|
923
|
+
name: "shelf",
|
|
924
|
+
parts: [
|
|
925
|
+
box("back", "plywood", [0.84, 1.5, 0.012], [0, 0.75, -0.14], { anchored: true }),
|
|
926
|
+
box("board_1", "wood_soft", [0.8, 0.025, 0.28], [0, 0.35, 0]),
|
|
927
|
+
box("board_2", "wood_soft", [0.8, 0.025, 0.28], [0, 0.75, 0]),
|
|
928
|
+
box("board_3", "wood_soft", [0.8, 0.025, 0.28], [0, 1.15, 0]),
|
|
929
|
+
box("board_4", "wood_soft", [0.8, 0.025, 0.28], [0, 1.5, 0])
|
|
930
|
+
],
|
|
931
|
+
joints: [joint("board_1", "back", "screw"), joint("board_2", "back", "screw"), joint("board_3", "back", "screw"), joint("board_4", "back", "screw")],
|
|
932
|
+
tags: ["furniture"]
|
|
933
|
+
});
|
|
934
|
+
var DEFS = [
|
|
935
|
+
{ id: "chair", group: "furniture", spec: chair, colors: { wood_soft: 11038778 } },
|
|
936
|
+
{ id: "sofa", group: "furniture", spec: sofa, colors: { frame: 7029798, seat_cushion: 8207180, back_cushion: 8207180, arm_l: 6958912, arm_r: 6958912 } },
|
|
937
|
+
{ id: "shelf", group: "furniture", spec: shelf, colors: { back: 14272931 }, movable: false },
|
|
938
|
+
{ id: "old_tv", group: "furniture", spec: oldTv, colors: { shell: 7166526, stand: 9067056, screen: 5205864 } },
|
|
939
|
+
{ id: "steel_sheet", group: "structure", spec: steelSheet, movable: false, colors: { sheet: 11910340 } },
|
|
940
|
+
{ id: "glass_pane", group: "structure", spec: glassPane, movable: false },
|
|
941
|
+
{ id: "concrete_block", group: "structure", spec: concreteBlock },
|
|
942
|
+
{ id: "brick_column", group: "structure", spec: brickColumn, movable: false },
|
|
943
|
+
{ id: "building_frame", group: "structure", spec: frame, movable: false, colors: { roof: 8224121 } },
|
|
944
|
+
{ id: "car_door", group: "vehicles", spec: carDoor, movable: false, colors: { skin: 12597547, window: 10278630 } },
|
|
945
|
+
{ id: "small_tree", group: "plants", spec: tree, movable: false },
|
|
946
|
+
{ id: "potted_plant", group: "plants", spec: pottedPlant, colors: { pot: 12871482 } },
|
|
947
|
+
{ id: "coffee_cup", group: "items", spec: cup, colors: { cup: 15986662 }, fluid: { container: { capacity: 0.6, liquid: "coffee", liters: 0.25, temperature: 80, shape: { kind: "cylinder", radius: 0.045, height: 0.1 }, opening: 0.045 }, part: "cup", uses: ["drink", "sip", "pour"], offset: [0, 0.01, 0] } },
|
|
948
|
+
{
|
|
949
|
+
id: "glass_bottle",
|
|
950
|
+
group: "items",
|
|
951
|
+
spec: bottle,
|
|
952
|
+
colors: { bottle_body: 8378032, bottle_neck: 8378032 },
|
|
953
|
+
fluid: { container: { capacity: 0.5, liquid: "water", liters: 0.5, temperature: 18, shape: { kind: "cylinder", radius: 0.034, height: 0.14 }, opening: 0.012 }, part: "bottle_body", uses: ["drink", "sip", "pour"], offset: [0, 0.015, 0] }
|
|
954
|
+
},
|
|
955
|
+
{ id: "wooden_crate", group: "containers", spec: crate },
|
|
956
|
+
{ id: "steel_drum", group: "containers", spec: drum, colors: { drum_body: 2842526, drum_top: 2842526, drum_bottom: 2842526 } },
|
|
957
|
+
{ id: "plastic_bucket", group: "containers", spec: bucket, colors: { bucket_body: 14239787, handle: 9079434 } },
|
|
958
|
+
{
|
|
959
|
+
id: "jerrycan",
|
|
960
|
+
group: "containers",
|
|
961
|
+
spec: jerrycan,
|
|
962
|
+
colors: { tank: 12729122, cap: 15909424, handle: 12729122 },
|
|
963
|
+
fluid: { container: { capacity: 20, liquid: "fuel", liters: 20, temperature: 20, shape: { kind: "box", size: [0.33, 0.44, 0.13] }, opening: 0.025, closed: true }, part: "tank", uses: ["pour", "open", "close"], offset: [0, 8e-3, 0] }
|
|
964
|
+
},
|
|
965
|
+
{ id: "gas_bottle", group: "containers", spec: gasBottle, colors: { cylinder: 14211280, valve: 7829367, foot_ring: 5592405 } }
|
|
966
|
+
];
|
|
967
|
+
function demoItems(locale = "en", extra = []) {
|
|
968
|
+
const s = resolveStrings(locale);
|
|
969
|
+
const out = DEFS.map((d) => ({
|
|
970
|
+
id: d.id,
|
|
971
|
+
name: s.items[d.id] ?? d.id,
|
|
972
|
+
group: s.groups[d.group] ?? d.group,
|
|
973
|
+
build: (THREE) => buildFromSpec(THREE, d.spec(), { colors: d.colors }),
|
|
974
|
+
spec: d.spec,
|
|
975
|
+
movable: d.movable,
|
|
976
|
+
fluid: d.fluid
|
|
977
|
+
}));
|
|
978
|
+
return [...out, ...extra];
|
|
979
|
+
}
|
|
980
|
+
var DEMO_ITEMS = demoItems("en");
|
|
981
|
+
|
|
982
|
+
// src/arena.ts
|
|
983
|
+
var tableSpec = () => {
|
|
984
|
+
const parts = [{ id: "top", material: "wood_hard", shape: { kind: "box", size: [1.6, 0.05, 0.8] }, position: [0, 0.755, 0] }];
|
|
985
|
+
const joints = [];
|
|
986
|
+
for (const [i, [x, z]] of [
|
|
987
|
+
[-0.72, -0.32],
|
|
988
|
+
[0.72, -0.32],
|
|
989
|
+
[-0.72, 0.32],
|
|
990
|
+
[0.72, 0.32]
|
|
991
|
+
].entries()) {
|
|
992
|
+
parts.push({ id: `leg_${i + 1}`, material: "wood_hard", shape: { kind: "box", size: [0.06, 0.73, 0.06] }, position: [x, 0.365, z], anchored: true });
|
|
993
|
+
joints.push({ a: `leg_${i + 1}`, b: "top", kind: "screw" });
|
|
994
|
+
}
|
|
995
|
+
return { name: "table", parts, joints, tags: ["furniture"] };
|
|
996
|
+
};
|
|
997
|
+
var shelfSpec = () => {
|
|
998
|
+
const parts = [];
|
|
999
|
+
const joints = [];
|
|
1000
|
+
for (const [i, x] of [-0.6, 0.6].entries()) {
|
|
1001
|
+
parts.push({ id: `upright_${i + 1}`, material: "wood_hard", shape: { kind: "box", size: [0.04, 1.7, 0.4] }, position: [x, 0.85, 0], anchored: true });
|
|
1002
|
+
}
|
|
1003
|
+
for (const [i, y] of [0.4, 0.9, 1.4].entries()) {
|
|
1004
|
+
parts.push({ id: `board_${i + 1}`, material: "wood_soft", shape: { kind: "box", size: [1.2, 0.03, 0.4] }, position: [0, y, 0] });
|
|
1005
|
+
joints.push({ a: `board_${i + 1}`, b: "upright_1", kind: "screw" }, { a: `board_${i + 1}`, b: "upright_2", kind: "screw" });
|
|
1006
|
+
}
|
|
1007
|
+
return { name: "shelf", parts, joints, tags: ["furniture"] };
|
|
1008
|
+
};
|
|
1009
|
+
function createArena(THREE, size, scene) {
|
|
1010
|
+
const group = new THREE.Group();
|
|
1011
|
+
group.name = "test-room-arena";
|
|
1012
|
+
scene.background = new THREE.Color(10466504);
|
|
1013
|
+
group.add(new THREE.HemisphereLight(13624319, 7037527, 0.75));
|
|
1014
|
+
const sun = new THREE.DirectionalLight(16777215, 2.1);
|
|
1015
|
+
sun.position.set(7, 13, 8);
|
|
1016
|
+
sun.castShadow = true;
|
|
1017
|
+
sun.shadow.mapSize.set(1024, 1024);
|
|
1018
|
+
const sc = sun.shadow.camera;
|
|
1019
|
+
sc.left = -16;
|
|
1020
|
+
sc.right = 16;
|
|
1021
|
+
sc.top = 16;
|
|
1022
|
+
sc.bottom = -16;
|
|
1023
|
+
sc.near = 1;
|
|
1024
|
+
sc.far = 40;
|
|
1025
|
+
sun.shadow.bias = -6e-4;
|
|
1026
|
+
group.add(sun);
|
|
1027
|
+
const floor = new THREE.Mesh(new THREE.PlaneGeometry(size * 2, size * 2), new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }));
|
|
1028
|
+
floor.rotation.x = -Math.PI / 2;
|
|
1029
|
+
floor.receiveShadow = true;
|
|
1030
|
+
floor.name = "floor";
|
|
1031
|
+
group.add(floor);
|
|
1032
|
+
const grid = new THREE.GridHelper(size * 2, size * 2, 5000274, 7829371);
|
|
1033
|
+
grid.position.y = 4e-3;
|
|
1034
|
+
group.add(grid);
|
|
1035
|
+
const wallZ = -11;
|
|
1036
|
+
const playerStart = [0, 1.7, 5];
|
|
1037
|
+
const pts = [];
|
|
1038
|
+
const rx = -1.6;
|
|
1039
|
+
for (let z = Math.round(playerStart[2]); z >= wallZ; z--) {
|
|
1040
|
+
const d = playerStart[2] - z;
|
|
1041
|
+
const len = d % 5 === 0 ? 0.5 : 0.2;
|
|
1042
|
+
pts.push(rx, 0.01, z, rx + len, 0.01, z);
|
|
1043
|
+
}
|
|
1044
|
+
pts.push(rx, 0.01, playerStart[2], rx, 0.01, wallZ);
|
|
1045
|
+
const rg = new THREE.BufferGeometry();
|
|
1046
|
+
rg.setAttribute("position", new THREE.Float32BufferAttribute(pts, 3));
|
|
1047
|
+
group.add(new THREE.LineSegments(rg, new THREE.LineBasicMaterial({ color: 16765503 })));
|
|
1048
|
+
for (let d = 5; playerStart[2] - d >= wallZ; d += 5) {
|
|
1049
|
+
try {
|
|
1050
|
+
const canvas = document.createElement("canvas");
|
|
1051
|
+
canvas.width = 128;
|
|
1052
|
+
canvas.height = 64;
|
|
1053
|
+
const ctx = canvas.getContext("2d");
|
|
1054
|
+
if (!ctx) break;
|
|
1055
|
+
ctx.fillStyle = "#ffd23f";
|
|
1056
|
+
ctx.font = "bold 44px sans-serif";
|
|
1057
|
+
ctx.textAlign = "center";
|
|
1058
|
+
ctx.fillText(`${d} m`, 64, 48);
|
|
1059
|
+
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(canvas), transparent: true, depthWrite: false }));
|
|
1060
|
+
sprite.scale.set(1.2, 0.6, 1);
|
|
1061
|
+
sprite.position.set(rx - 0.8, 0.45, playerStart[2] - d);
|
|
1062
|
+
group.add(sprite);
|
|
1063
|
+
} catch {
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const tableX = 3.5;
|
|
1068
|
+
const tapMat = new THREE.MeshStandardMaterial({ color: 12172996, metalness: 0.8, roughness: 0.35 });
|
|
1069
|
+
const post = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 1.4, 10), tapMat);
|
|
1070
|
+
post.position.set(tableX + 1.15, 0.7, 0);
|
|
1071
|
+
const arm = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 1.15 - 0.3, 10), tapMat);
|
|
1072
|
+
arm.rotation.z = Math.PI / 2;
|
|
1073
|
+
arm.position.set(tableX + (1.15 + 0.3) / 2, 1.38, 0);
|
|
1074
|
+
const spoutMesh = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.1, 10), tapMat);
|
|
1075
|
+
spoutMesh.position.set(tableX + 0.3, 1.33, 0);
|
|
1076
|
+
for (const m of [post, arm, spoutMesh]) {
|
|
1077
|
+
m.castShadow = true;
|
|
1078
|
+
group.add(m);
|
|
1079
|
+
}
|
|
1080
|
+
const tapSpout = [tableX + 0.3, 1.28, 0];
|
|
1081
|
+
const objects = [
|
|
1082
|
+
{ id: "target_wall", spec: brickWallSpec(), position: [0, 0, wallZ], colors: { brick: 10831662 } },
|
|
1083
|
+
{ id: "table", spec: tableSpec(), position: [tableX, 0, 0] },
|
|
1084
|
+
{ id: "shelf", spec: shelfSpec(), position: [-3.5, 0, 0], colors: { wood_soft: 13081186 } }
|
|
1085
|
+
];
|
|
1086
|
+
return {
|
|
1087
|
+
group,
|
|
1088
|
+
objects,
|
|
1089
|
+
surfaces: [
|
|
1090
|
+
{ id: "floor", point: [0, 0, 0], normal: [0, 1, 0], halfSize: [size, size], porosity: 0.05 },
|
|
1091
|
+
{ id: "table", point: [tableX, 0.78, 0], normal: [0, 1, 0], halfSize: [0.8, 0.4], porosity: 0.3 }
|
|
1092
|
+
],
|
|
1093
|
+
colliders: [
|
|
1094
|
+
[tableX - 0.8, -0.4, tableX + 0.8, 0.4],
|
|
1095
|
+
[tableX + 1.1, -0.1, tableX + 1.2, 0.1],
|
|
1096
|
+
[-3.5 - 0.62, -0.2, -3.5 + 0.62, 0.2],
|
|
1097
|
+
[-size, wallZ - 1, size, wallZ + 0.2]
|
|
1098
|
+
],
|
|
1099
|
+
playerStart,
|
|
1100
|
+
wallZ,
|
|
1101
|
+
table: { minX: tableX - 0.8, maxX: tableX + 0.8, minZ: -0.4, maxZ: 0.4, top: 0.78, id: "table", topPart: "top" },
|
|
1102
|
+
tapSpout,
|
|
1103
|
+
sun
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/log.ts
|
|
1108
|
+
var MAX_ENTRIES = 5e3;
|
|
1109
|
+
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
1110
|
+
var formatTime = (t) => {
|
|
1111
|
+
const m = Math.floor(t / 60);
|
|
1112
|
+
const s = t - m * 60;
|
|
1113
|
+
return `${pad(m)}:${s.toFixed(2).padStart(5, "0")}`;
|
|
1114
|
+
};
|
|
1115
|
+
function createLog() {
|
|
1116
|
+
let list = [];
|
|
1117
|
+
let seq = 0;
|
|
1118
|
+
let clock = () => 0;
|
|
1119
|
+
let beforeRead = null;
|
|
1120
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1121
|
+
const keyed = /* @__PURE__ */ new Map();
|
|
1122
|
+
const read = () => {
|
|
1123
|
+
beforeRead?.();
|
|
1124
|
+
return list;
|
|
1125
|
+
};
|
|
1126
|
+
const emit = (e, updated) => {
|
|
1127
|
+
for (const fn of listeners) {
|
|
1128
|
+
try {
|
|
1129
|
+
fn(e, updated);
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const add = (entry) => {
|
|
1135
|
+
const { t, ...rest } = entry;
|
|
1136
|
+
const e = { seq: seq++, t: Math.round((t ?? clock()) * 1e3) / 1e3, ...rest };
|
|
1137
|
+
list.push(e);
|
|
1138
|
+
if (list.length > MAX_ENTRIES) list = list.slice(list.length - MAX_ENTRIES);
|
|
1139
|
+
emit(e, false);
|
|
1140
|
+
return e;
|
|
1141
|
+
};
|
|
1142
|
+
const log = {
|
|
1143
|
+
add,
|
|
1144
|
+
coalesce(key, entry, windowS = 1) {
|
|
1145
|
+
const now = entry.t ?? clock();
|
|
1146
|
+
const k = keyed.get(key);
|
|
1147
|
+
if (k && now - k.last <= windowS && list.includes(k.entry)) {
|
|
1148
|
+
const { t: _t, ...rest } = entry;
|
|
1149
|
+
k.count++;
|
|
1150
|
+
Object.assign(k.entry, rest);
|
|
1151
|
+
k.entry.data = { ...rest.data ?? {}, updates: k.count };
|
|
1152
|
+
k.last = now;
|
|
1153
|
+
emit(k.entry, true);
|
|
1154
|
+
return k.entry;
|
|
1155
|
+
}
|
|
1156
|
+
const e = add(entry);
|
|
1157
|
+
keyed.set(key, { entry: e, last: now, count: 1 });
|
|
1158
|
+
return e;
|
|
1159
|
+
},
|
|
1160
|
+
setBeforeRead(fn) {
|
|
1161
|
+
beforeRead = fn;
|
|
1162
|
+
},
|
|
1163
|
+
setClock(fn) {
|
|
1164
|
+
clock = fn;
|
|
1165
|
+
},
|
|
1166
|
+
entries(filter) {
|
|
1167
|
+
const all = read();
|
|
1168
|
+
if (!filter) return all.slice();
|
|
1169
|
+
return all.filter(
|
|
1170
|
+
(e) => (!filter.channel || e.channel === filter.channel) && (!filter.type || e.type === filter.type) && (!filter.objectId || e.objectId === filter.objectId)
|
|
1171
|
+
);
|
|
1172
|
+
},
|
|
1173
|
+
clear() {
|
|
1174
|
+
beforeRead?.();
|
|
1175
|
+
list = [];
|
|
1176
|
+
keyed.clear();
|
|
1177
|
+
},
|
|
1178
|
+
toJSON: () => JSON.stringify(read(), null, 2),
|
|
1179
|
+
toNDJSON: () => read().map((e) => JSON.stringify(e)).join("\n") + (list.length ? "\n" : ""),
|
|
1180
|
+
toText: () => read().map((e) => `[${formatTime(e.t)}] ${e.channel.padEnd(6)} ${e.text}`).join("\n"),
|
|
1181
|
+
download(format = "ndjson") {
|
|
1182
|
+
if (typeof document === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL) return;
|
|
1183
|
+
const body = format === "json" ? log.toJSON() : format === "txt" ? log.toText() : log.toNDJSON();
|
|
1184
|
+
const mime = format === "json" ? "application/json" : "text/plain";
|
|
1185
|
+
const url = URL.createObjectURL(new Blob([body], { type: mime }));
|
|
1186
|
+
const a = document.createElement("a");
|
|
1187
|
+
a.href = url;
|
|
1188
|
+
a.download = `test-room-log.${format}`;
|
|
1189
|
+
document.body.appendChild(a);
|
|
1190
|
+
a.click();
|
|
1191
|
+
a.remove();
|
|
1192
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
1193
|
+
},
|
|
1194
|
+
summary() {
|
|
1195
|
+
const byType = {};
|
|
1196
|
+
const byMaterial = {};
|
|
1197
|
+
const objects = /* @__PURE__ */ new Set();
|
|
1198
|
+
let destroyed = 0;
|
|
1199
|
+
for (const e of read()) {
|
|
1200
|
+
byType[e.type] = (byType[e.type] ?? 0) + 1;
|
|
1201
|
+
if (e.material) byMaterial[e.material] = (byMaterial[e.material] ?? 0) + 1;
|
|
1202
|
+
if (e.objectId) objects.add(e.objectId);
|
|
1203
|
+
if (e.type === "destroyed") destroyed++;
|
|
1204
|
+
}
|
|
1205
|
+
return { byType, byMaterial, objects: objects.size, destroyed };
|
|
1206
|
+
},
|
|
1207
|
+
on(fn) {
|
|
1208
|
+
listeners.add(fn);
|
|
1209
|
+
return () => listeners.delete(fn);
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
return log;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/narrate.ts
|
|
1216
|
+
var SKIP_DAMAGE = /* @__PURE__ */ new Set(["fire", "heat"]);
|
|
1217
|
+
function createNarrator(c) {
|
|
1218
|
+
const { t, num } = c;
|
|
1219
|
+
const materialMemo = /* @__PURE__ */ new Map();
|
|
1220
|
+
const findPart = (objectId, partId) => {
|
|
1221
|
+
const p = c.world.part(objectId, partId) ?? c.locatePart?.(objectId, partId)?.part ?? null;
|
|
1222
|
+
if (p) materialMemo.set(`${objectId}|${partId}`, p.material);
|
|
1223
|
+
return p;
|
|
1224
|
+
};
|
|
1225
|
+
const plural = (key, n) => t(`${key}.${n === 1 ? "one" : "other"}`, { n });
|
|
1226
|
+
const pieces = (n) => plural("count.piece", n);
|
|
1227
|
+
const joints = (n) => plural("count.joint", n);
|
|
1228
|
+
const xz = (x, z) => t("coords", { x: num(x, 1), z: num(z, 1) });
|
|
1229
|
+
const where = (objectId, partId) => {
|
|
1230
|
+
const p = findPart(objectId, partId);
|
|
1231
|
+
const m = p?.material ?? materialMemo.get(`${objectId}|${partId}`);
|
|
1232
|
+
return `${c.nameOf(objectId)} \u203A ${partId}${m ? ` (${c.materialName(m)})` : ""}`;
|
|
1233
|
+
};
|
|
1234
|
+
const stateOf = (objectId, partId) => {
|
|
1235
|
+
const p = findPart(objectId, partId);
|
|
1236
|
+
if (!p) return void 0;
|
|
1237
|
+
const s = {
|
|
1238
|
+
status: p.status,
|
|
1239
|
+
hp: Math.round(p.hp * 10) / 10,
|
|
1240
|
+
maxHp: Math.round(p.maxHp * 10) / 10,
|
|
1241
|
+
temperature: Math.round(p.temperature),
|
|
1242
|
+
char: Math.round(p.char * 100) / 100,
|
|
1243
|
+
melt: Math.round(p.melt * 100) / 100
|
|
1244
|
+
};
|
|
1245
|
+
if (p.liters !== void 0) s.liters = Math.round(p.liters * 1e3) / 1e3;
|
|
1246
|
+
return s;
|
|
1247
|
+
};
|
|
1248
|
+
const base = (ev, part) => ({
|
|
1249
|
+
channel: "event",
|
|
1250
|
+
objectId: ev.object,
|
|
1251
|
+
itemId: c.itemIdOf(ev.object),
|
|
1252
|
+
part,
|
|
1253
|
+
material: part ? findPart(ev.object, part)?.material ?? materialMemo.get(`${ev.object}|${part}`) : void 0,
|
|
1254
|
+
state: part ? stateOf(ev.object, part) : void 0
|
|
1255
|
+
});
|
|
1256
|
+
const hpText = (obj, part) => {
|
|
1257
|
+
const p = findPart(obj, part);
|
|
1258
|
+
return p ? `hp ${num(p.hp, 0)}/${num(p.maxHp, 0)}` : "";
|
|
1259
|
+
};
|
|
1260
|
+
const isLost = (obj, part) => {
|
|
1261
|
+
const st = findPart(obj, part)?.status;
|
|
1262
|
+
return st === "broken" || st === "melted" || st === "ashes";
|
|
1263
|
+
};
|
|
1264
|
+
const crackAcc = /* @__PURE__ */ new Map();
|
|
1265
|
+
const damageAcc = /* @__PURE__ */ new Map();
|
|
1266
|
+
const stressAt = /* @__PURE__ */ new Map();
|
|
1267
|
+
const leakAt = /* @__PURE__ */ new Map();
|
|
1268
|
+
const list = (names) => names.length > 6 ? `${names.slice(0, 6).join(", ")}\u2026` : names.join(", ");
|
|
1269
|
+
const events = (batch) => {
|
|
1270
|
+
const now = c.now();
|
|
1271
|
+
const damage = /* @__PURE__ */ new Map();
|
|
1272
|
+
const holes = /* @__PURE__ */ new Map();
|
|
1273
|
+
const dents = /* @__PURE__ */ new Map();
|
|
1274
|
+
for (const ev of batch) {
|
|
1275
|
+
if (ev.type !== "damage") continue;
|
|
1276
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1277
|
+
const prev = damage.get(k);
|
|
1278
|
+
damage.set(k, { amount: (prev?.amount ?? 0) + ev.amount, type: ev.damageType });
|
|
1279
|
+
}
|
|
1280
|
+
const lostJoints = /* @__PURE__ */ new Map();
|
|
1281
|
+
const openJoints = /* @__PURE__ */ new Map();
|
|
1282
|
+
const fireParts = /* @__PURE__ */ new Map();
|
|
1283
|
+
const outParts = /* @__PURE__ */ new Map();
|
|
1284
|
+
for (const ev of batch) {
|
|
1285
|
+
if (ev.type === "detach") {
|
|
1286
|
+
if (ev.by === "part_lost") {
|
|
1287
|
+
const [a, b] = ev.parts;
|
|
1288
|
+
const lost = isLost(ev.object, a) && !isLost(ev.object, b) ? a : isLost(ev.object, b) && !isLost(ev.object, a) ? b : isLost(ev.object, a) ? a : b;
|
|
1289
|
+
const key = `${ev.object}|${lost}`;
|
|
1290
|
+
const g = lostJoints.get(key) ?? { obj: ev.object, part: lost, joints: 0, groups: 0, merged: false };
|
|
1291
|
+
g.joints++;
|
|
1292
|
+
g.groups = ev.groups.length;
|
|
1293
|
+
lostJoints.set(key, g);
|
|
1294
|
+
} else {
|
|
1295
|
+
const key = `${ev.object}|${ev.by}`;
|
|
1296
|
+
const g = openJoints.get(key) ?? { obj: ev.object, by: ev.by, list: [], groups: 0, done: false };
|
|
1297
|
+
const [a, b] = ev.parts;
|
|
1298
|
+
g.list.push({ joint: ev.joint, a, b });
|
|
1299
|
+
g.groups = ev.groups.length;
|
|
1300
|
+
openJoints.set(key, g);
|
|
1301
|
+
}
|
|
1302
|
+
} else if (ev.type === "ignite") {
|
|
1303
|
+
const g = fireParts.get(ev.object) ?? { obj: ev.object, parts: [], temp: 0, done: false };
|
|
1304
|
+
g.parts.push(ev.part);
|
|
1305
|
+
g.temp = Math.max(g.temp, findPart(ev.object, ev.part)?.temperature ?? 0);
|
|
1306
|
+
fireParts.set(ev.object, g);
|
|
1307
|
+
} else if (ev.type === "extinguish") {
|
|
1308
|
+
const g = outParts.get(ev.object) ?? { obj: ev.object, parts: [], done: false };
|
|
1309
|
+
g.parts.push(ev.part);
|
|
1310
|
+
outParts.set(ev.object, g);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const lostTail = (key) => {
|
|
1314
|
+
const g = lostJoints.get(key);
|
|
1315
|
+
if (!g) return "";
|
|
1316
|
+
g.merged = true;
|
|
1317
|
+
return t("ev.lost.tail", { joints: joints(g.joints), pieces: pieces(g.groups) });
|
|
1318
|
+
};
|
|
1319
|
+
const lostData = (key) => {
|
|
1320
|
+
const g = lostJoints.get(key);
|
|
1321
|
+
return g ? { jointsOpened: g.joints, pieces: g.groups } : {};
|
|
1322
|
+
};
|
|
1323
|
+
for (const ev of batch) {
|
|
1324
|
+
switch (ev.type) {
|
|
1325
|
+
case "damage":
|
|
1326
|
+
break;
|
|
1327
|
+
case "dent": {
|
|
1328
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1329
|
+
const d = damage.get(k);
|
|
1330
|
+
damage.delete(k);
|
|
1331
|
+
const mm = ev.dent.depth * 1e3;
|
|
1332
|
+
const prev = dents.get(k);
|
|
1333
|
+
if (prev) {
|
|
1334
|
+
const n = Number(prev.data?.count ?? 1) + 1;
|
|
1335
|
+
const deepest = Math.max(Number(prev.data?.depthMm ?? 0), mm);
|
|
1336
|
+
prev.text = t("ev.dents", { where: where(ev.object, ev.part), n, mm: num(deepest, 0), hp: hpText(ev.object, ev.part) });
|
|
1337
|
+
prev.data = { ...prev.data, count: n, depthMm: Math.round(deepest * 10) / 10 };
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
dents.set(
|
|
1341
|
+
k,
|
|
1342
|
+
c.log.add({
|
|
1343
|
+
...base(ev, ev.part),
|
|
1344
|
+
type: "dent",
|
|
1345
|
+
text: t("ev.dent", { where: where(ev.object, ev.part), mm: num(mm, 0), hp: hpText(ev.object, ev.part) }),
|
|
1346
|
+
data: { count: 1, depthMm: Math.round(mm * 10) / 10, radius: ev.dent.radius, damage: d?.amount, point: ev.worldPoint, source: ev.source }
|
|
1347
|
+
})
|
|
1348
|
+
);
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
case "hole": {
|
|
1352
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1353
|
+
const d = damage.get(k);
|
|
1354
|
+
damage.delete(k);
|
|
1355
|
+
const mm = ev.hole.diameter * 1e3;
|
|
1356
|
+
const prev = holes.get(k);
|
|
1357
|
+
if (prev) {
|
|
1358
|
+
const n = Number(prev.data?.count ?? 1) + 1;
|
|
1359
|
+
const through = Number(prev.data?.throughCount ?? 0) + (ev.hole.through ? 1 : 0);
|
|
1360
|
+
prev.text = t("ev.holes", { where: where(ev.object, ev.part), n, mm: num(mm, 0), thr: through, hp: hpText(ev.object, ev.part) });
|
|
1361
|
+
prev.data = { ...prev.data, count: n, throughCount: through, through: through > 0 };
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
holes.set(
|
|
1365
|
+
k,
|
|
1366
|
+
c.log.add({
|
|
1367
|
+
...base(ev, ev.part),
|
|
1368
|
+
type: "hole",
|
|
1369
|
+
text: t(ev.hole.through ? "ev.hole.through" : "ev.hole.stuck", { where: where(ev.object, ev.part), mm: num(mm, 0), hp: hpText(ev.object, ev.part) }),
|
|
1370
|
+
data: { count: 1, throughCount: ev.hole.through ? 1 : 0, diameterMm: Math.round(mm * 10) / 10, through: ev.hole.through, damage: d?.amount, point: ev.worldPoint, source: ev.source }
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
case "crack": {
|
|
1376
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1377
|
+
const d = damage.get(k);
|
|
1378
|
+
damage.delete(k);
|
|
1379
|
+
const loaded = (findPart(ev.object, ev.part)?.stress ?? 0) > 0.8 && ev.source === void 0;
|
|
1380
|
+
const cm = ev.crack.length * 100;
|
|
1381
|
+
if (loaded) {
|
|
1382
|
+
const acc = crackAcc.get(k);
|
|
1383
|
+
const n = acc && now - acc.last <= 1 ? acc.n + 1 : 1;
|
|
1384
|
+
const longest = acc && n > 1 ? Math.max(acc.cm, cm) : cm;
|
|
1385
|
+
crackAcc.set(k, { n, cm: longest, last: now });
|
|
1386
|
+
c.log.coalesce(
|
|
1387
|
+
`crack:${k}`,
|
|
1388
|
+
{
|
|
1389
|
+
...base(ev, ev.part),
|
|
1390
|
+
type: "crack",
|
|
1391
|
+
text: t("ev.cracks", { where: where(ev.object, ev.part), n, cm: num(longest, 0), hp: hpText(ev.object, ev.part) }),
|
|
1392
|
+
data: { rays: ev.crack.rays, lengthM: ev.crack.length, count: n, underLoad: true }
|
|
1393
|
+
},
|
|
1394
|
+
1
|
|
1395
|
+
);
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
c.log.add({
|
|
1399
|
+
...base(ev, ev.part),
|
|
1400
|
+
type: "crack",
|
|
1401
|
+
text: t("ev.crack", { where: where(ev.object, ev.part), rays: ev.crack.rays, cm: num(cm, 0), hp: hpText(ev.object, ev.part) }),
|
|
1402
|
+
data: { rays: ev.crack.rays, lengthM: ev.crack.length, damage: d?.amount, source: ev.source }
|
|
1403
|
+
});
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
case "ricochet":
|
|
1407
|
+
c.log.add({
|
|
1408
|
+
...base(ev, ev.part),
|
|
1409
|
+
type: "ricochet",
|
|
1410
|
+
text: t("ev.ricochet", { where: where(ev.object, ev.part), energy: num(ev.energy, 0) }),
|
|
1411
|
+
data: { energy: ev.energy, direction: ev.direction }
|
|
1412
|
+
});
|
|
1413
|
+
break;
|
|
1414
|
+
case "leak": {
|
|
1415
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1416
|
+
const liquid = c.liquidName(ev.fluid);
|
|
1417
|
+
const hole = holes.get(k);
|
|
1418
|
+
const rate = num(ev.rate, 2);
|
|
1419
|
+
leakAt.set(ev.object, now);
|
|
1420
|
+
if (hole) {
|
|
1421
|
+
hole.text += t("ev.leak.tail", { liquid, rate });
|
|
1422
|
+
hole.data = { ...hole.data, leak: { fluid: ev.fluid, ratePerS: ev.rate, pressurized: ev.pressurized } };
|
|
1423
|
+
} else {
|
|
1424
|
+
c.log.add({ ...base(ev, ev.part), type: "leak", text: t("ev.leak.alone", { where: where(ev.object, ev.part), liquid, rate }), data: { fluid: ev.fluid, ratePerS: ev.rate } });
|
|
1425
|
+
}
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
case "fracture": {
|
|
1429
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1430
|
+
const d = damage.get(k);
|
|
1431
|
+
damage.delete(k);
|
|
1432
|
+
c.log.add({
|
|
1433
|
+
...base(ev, ev.part),
|
|
1434
|
+
type: "fracture",
|
|
1435
|
+
text: t(`ev.fracture.${ev.style}`, { where: where(ev.object, ev.part), n: ev.fragments.length }) + lostTail(k),
|
|
1436
|
+
data: { style: ev.style, fragments: ev.fragments.length, damage: d?.amount, point: ev.point, source: ev.source, ...lostData(k) }
|
|
1437
|
+
});
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
case "detach": {
|
|
1441
|
+
if (ev.by === "part_lost") {
|
|
1442
|
+
const [a2, b2] = ev.parts;
|
|
1443
|
+
const lost = isLost(ev.object, a2) && !isLost(ev.object, b2) ? a2 : isLost(ev.object, b2) && !isLost(ev.object, a2) ? b2 : isLost(ev.object, a2) ? a2 : b2;
|
|
1444
|
+
const key2 = `${ev.object}|${lost}`;
|
|
1445
|
+
const g2 = lostJoints.get(key2);
|
|
1446
|
+
if (!g2 || g2.merged) break;
|
|
1447
|
+
const hasOwnLine = batch.some((e) => (e.type === "fracture" || e.type === "burnout") && e.object === ev.object && e.part === lost);
|
|
1448
|
+
if (hasOwnLine) break;
|
|
1449
|
+
g2.merged = true;
|
|
1450
|
+
const status = findPart(ev.object, lost)?.status;
|
|
1451
|
+
const verb = t(`lost.${status === "ashes" ? "ashes" : status === "melted" ? "melted" : "broken"}`);
|
|
1452
|
+
c.log.add({
|
|
1453
|
+
...base(ev, lost),
|
|
1454
|
+
type: "detach",
|
|
1455
|
+
text: t("ev.lost.alone", { where: where(ev.object, lost), verb, joints: joints(g2.joints), pieces: pieces(g2.groups) }),
|
|
1456
|
+
data: { parts: [a2, b2], by: ev.by, ...lostData(key2) }
|
|
1457
|
+
});
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
const key = `${ev.object}|${ev.by}`;
|
|
1461
|
+
const g = openJoints.get(key);
|
|
1462
|
+
if (!g || g.done) break;
|
|
1463
|
+
g.done = true;
|
|
1464
|
+
const [a, b] = ev.parts;
|
|
1465
|
+
const many = g.list.length > 1;
|
|
1466
|
+
c.log.add({
|
|
1467
|
+
channel: "event",
|
|
1468
|
+
type: "detach",
|
|
1469
|
+
objectId: ev.object,
|
|
1470
|
+
itemId: c.itemIdOf(ev.object),
|
|
1471
|
+
text: many ? t(`ev.detach.many.${ev.by === "tool" ? "tool" : "break"}`, { obj: c.nameOf(ev.object), joints: joints(g.list.length), pieces: pieces(g.groups) }) : t("ev.detach", { obj: c.nameOf(ev.object), a, b, by: t(`by.${ev.by}`), pieces: pieces(g.groups) }),
|
|
1472
|
+
data: { joint: ev.joint, joints: g.list.map((j) => j.joint), parts: ev.parts, by: ev.by, groups: g.groups }
|
|
1473
|
+
});
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
case "disassemble_progress": {
|
|
1477
|
+
const pct = Math.round(ev.progress * 100);
|
|
1478
|
+
const kind = kindOf(ev.object, ev.joint);
|
|
1479
|
+
const [a, b] = ev.joint.split("~");
|
|
1480
|
+
c.log.coalesce(
|
|
1481
|
+
`dis:${ev.object}:${ev.joint}`,
|
|
1482
|
+
{
|
|
1483
|
+
channel: "event",
|
|
1484
|
+
type: "disassemble_progress",
|
|
1485
|
+
objectId: ev.object,
|
|
1486
|
+
itemId: c.itemIdOf(ev.object),
|
|
1487
|
+
text: t("ev.disassemble", { obj: c.nameOf(ev.object), a: a ?? ev.joint, b: b ?? "", kind: kind ? c.jointKind(kind) : t("joint.generic"), tool: c.toolName(ev.tool), pct }),
|
|
1488
|
+
data: { joint: ev.joint, tool: ev.tool, progress: ev.progress }
|
|
1489
|
+
},
|
|
1490
|
+
1.5
|
|
1491
|
+
);
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
case "ignite": {
|
|
1495
|
+
const g = fireParts.get(ev.object);
|
|
1496
|
+
if (g && g.parts.length >= 3) {
|
|
1497
|
+
if (g.done) break;
|
|
1498
|
+
g.done = true;
|
|
1499
|
+
c.log.add({
|
|
1500
|
+
channel: "event",
|
|
1501
|
+
type: "ignite",
|
|
1502
|
+
objectId: ev.object,
|
|
1503
|
+
itemId: c.itemIdOf(ev.object),
|
|
1504
|
+
text: t("ev.ignite.many", { obj: c.nameOf(ev.object), n: g.parts.length, temp: num(g.temp, 0), parts: list(g.parts) }),
|
|
1505
|
+
data: { parts: g.parts, temperature: Math.round(g.temp) }
|
|
1506
|
+
});
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
c.log.add({ ...base(ev, ev.part), type: "ignite", text: t("ev.ignite", { where: where(ev.object, ev.part), temp: num(findPart(ev.object, ev.part)?.temperature ?? 0, 0) }) });
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
case "extinguish": {
|
|
1513
|
+
const g = outParts.get(ev.object);
|
|
1514
|
+
if (g && g.parts.length >= 3) {
|
|
1515
|
+
if (g.done) break;
|
|
1516
|
+
g.done = true;
|
|
1517
|
+
c.log.add({
|
|
1518
|
+
channel: "event",
|
|
1519
|
+
type: "extinguish",
|
|
1520
|
+
objectId: ev.object,
|
|
1521
|
+
itemId: c.itemIdOf(ev.object),
|
|
1522
|
+
text: t("ev.extinguish.many", { obj: c.nameOf(ev.object), n: g.parts.length, parts: list(g.parts) }),
|
|
1523
|
+
data: { parts: g.parts }
|
|
1524
|
+
});
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
c.log.add({ ...base(ev, ev.part), type: "extinguish", text: t("ev.extinguish", { where: where(ev.object, ev.part) }) });
|
|
1528
|
+
break;
|
|
1529
|
+
}
|
|
1530
|
+
case "burnout": {
|
|
1531
|
+
const k = `${ev.object}|${ev.part}`;
|
|
1532
|
+
c.log.add({ ...base(ev, ev.part), type: "burnout", text: t("ev.burnout", { where: where(ev.object, ev.part) }) + lostTail(k), data: lostData(k) });
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
case "melt":
|
|
1536
|
+
c.log.coalesce(
|
|
1537
|
+
`melt:${ev.object}:${ev.part}`,
|
|
1538
|
+
{ ...base(ev, ev.part), type: "melt", text: t("ev.melt", { where: where(ev.object, ev.part), pct: Math.round(ev.melt * 100) }), data: { melt: ev.melt } },
|
|
1539
|
+
2
|
|
1540
|
+
);
|
|
1541
|
+
break;
|
|
1542
|
+
case "drip":
|
|
1543
|
+
c.log.coalesce(`drip:${ev.object}:${ev.part}`, { ...base(ev, ev.part), type: "drip", text: t("ev.drip", { where: where(ev.object, ev.part) }), data: { point: ev.point } }, 2);
|
|
1544
|
+
break;
|
|
1545
|
+
case "explode":
|
|
1546
|
+
c.log.add({
|
|
1547
|
+
...base(ev, ev.part),
|
|
1548
|
+
type: "explode",
|
|
1549
|
+
text: t("ev.explode", { where: where(ev.object, ev.part), energy: num(ev.energy, 0), radius: num(ev.radius, 1) }),
|
|
1550
|
+
data: { energy: ev.energy, radius: ev.radius, point: ev.point }
|
|
1551
|
+
});
|
|
1552
|
+
break;
|
|
1553
|
+
case "destroyed":
|
|
1554
|
+
c.log.add({ channel: "event", type: "destroyed", objectId: ev.object, itemId: c.itemIdOf(ev.object), text: t("ev.destroyed", { obj: c.nameOf(ev.object) }) });
|
|
1555
|
+
break;
|
|
1556
|
+
default: {
|
|
1557
|
+
const x = ev;
|
|
1558
|
+
if (x.type === "stress") {
|
|
1559
|
+
const part = String(x.part ?? "");
|
|
1560
|
+
stressAt.set(`${x.object}|${part}`, now);
|
|
1561
|
+
c.log.coalesce(
|
|
1562
|
+
`stress:${x.object}:${part}`,
|
|
1563
|
+
{
|
|
1564
|
+
...base(ev, part),
|
|
1565
|
+
type: "stress",
|
|
1566
|
+
text: t("ev.stress", { where: where(x.object, part), stress: num(Number(x.stress ?? 0) * 100, 0), load: num(Number(x.load ?? 0), 0) }),
|
|
1567
|
+
data: { stress: x.stress, load: x.load }
|
|
1568
|
+
},
|
|
1569
|
+
1
|
|
1570
|
+
);
|
|
1571
|
+
} else if (x.type === "collapse") {
|
|
1572
|
+
const parts = Array.isArray(x.parts) ? x.parts.map(String) : [];
|
|
1573
|
+
c.log.add({
|
|
1574
|
+
channel: "event",
|
|
1575
|
+
type: "collapse",
|
|
1576
|
+
objectId: x.object,
|
|
1577
|
+
itemId: c.itemIdOf(x.object),
|
|
1578
|
+
text: t("ev.collapse", { obj: c.nameOf(x.object), pieces: pieces(parts.length), mass: num(Number(x.mass ?? 0), 0), parts: list(parts) }),
|
|
1579
|
+
data: { parts: parts.length, partIds: parts, massKg: x.mass, cause: x.cause }
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
for (const [k, d] of damage) {
|
|
1586
|
+
if (SKIP_DAMAGE.has(d.type) || d.amount < 0.5) continue;
|
|
1587
|
+
const [obj, part] = k.split("|");
|
|
1588
|
+
const overload = d.type === "blunt" && now - (stressAt.get(k) ?? -99) <= 3;
|
|
1589
|
+
const entry = {
|
|
1590
|
+
channel: "event",
|
|
1591
|
+
type: "damage",
|
|
1592
|
+
objectId: obj,
|
|
1593
|
+
itemId: c.itemIdOf(obj),
|
|
1594
|
+
part,
|
|
1595
|
+
material: findPart(obj, part)?.material ?? materialMemo.get(k),
|
|
1596
|
+
state: stateOf(obj, part)
|
|
1597
|
+
};
|
|
1598
|
+
if (!overload) {
|
|
1599
|
+
c.log.add({ ...entry, text: t("ev.damage", { where: where(obj, part), type: t(`dmg.${d.type}`), amount: num(d.amount, 0), hp: hpText(obj, part) }), data: { amount: d.amount, damageType: d.type } });
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
const acc = damageAcc.get(k);
|
|
1603
|
+
const total = acc && now - acc.last <= 1 ? acc.total + d.amount : d.amount;
|
|
1604
|
+
damageAcc.set(k, { total, last: now });
|
|
1605
|
+
c.log.coalesce(
|
|
1606
|
+
`damage:${k}`,
|
|
1607
|
+
{ ...entry, text: t("ev.damage.overload", { where: where(obj, part), amount: num(total, 0), hp: hpText(obj, part) }), data: { amount: total, damageType: d.type, overload: true } },
|
|
1608
|
+
1
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
const kindMemo = /* @__PURE__ */ new Map();
|
|
1613
|
+
const kindOf = (objectId, jointId) => {
|
|
1614
|
+
const key = `${objectId}|${jointId}`;
|
|
1615
|
+
const own = c.world.state(objectId)?.joints.find((j) => j.id === jointId)?.kind;
|
|
1616
|
+
if (own) {
|
|
1617
|
+
kindMemo.set(key, own);
|
|
1618
|
+
return own;
|
|
1619
|
+
}
|
|
1620
|
+
const known = kindMemo.get(key);
|
|
1621
|
+
if (known) return known;
|
|
1622
|
+
for (const id of c.world.ids()) {
|
|
1623
|
+
const k = c.world.state(id)?.joints.find((j) => j.id === jointId)?.kind;
|
|
1624
|
+
if (k) return k;
|
|
1625
|
+
}
|
|
1626
|
+
return "";
|
|
1627
|
+
};
|
|
1628
|
+
const stainInfo = /* @__PURE__ */ new Map();
|
|
1629
|
+
const stainAcc = /* @__PURE__ */ new Map();
|
|
1630
|
+
const driedAcc = /* @__PURE__ */ new Map();
|
|
1631
|
+
const drinkAcc = /* @__PURE__ */ new Map();
|
|
1632
|
+
const fluid = (ev) => {
|
|
1633
|
+
const who = (id) => c.containerName(id)?.name ?? id;
|
|
1634
|
+
const meta = (id) => {
|
|
1635
|
+
const m = c.containerName(id);
|
|
1636
|
+
return { objectId: m?.objectId, itemId: m?.itemId };
|
|
1637
|
+
};
|
|
1638
|
+
const f = "fluid.";
|
|
1639
|
+
switch (ev.type) {
|
|
1640
|
+
case "fill":
|
|
1641
|
+
c.log.coalesce(`fill:${ev.container}`, { channel: "event", type: f + "fill", ...meta(ev.container), text: t("fl.fill", { who: who(ev.container), liquid: c.liquidName(ev.liquid), liters: num(ev.liters, 2) }), data: { liters: ev.liters } }, 1.5);
|
|
1642
|
+
break;
|
|
1643
|
+
case "pour":
|
|
1644
|
+
c.log.coalesce(`pour:${ev.container}`, { channel: "event", type: f + "pour", ...meta(ev.container), text: t("fl.pour", { who: who(ev.container), liquid: c.liquidName(ev.liquid), rate: num(ev.rate, 2) }), data: { rate: ev.rate } }, 2);
|
|
1645
|
+
break;
|
|
1646
|
+
case "spill":
|
|
1647
|
+
c.log.coalesce(`spill:${ev.container}`, { channel: "event", type: f + "spill", ...meta(ev.container), text: t("fl.spill", { who: who(ev.container), liquid: c.liquidName(ev.liquid), rate: num(ev.rate, 2) }), data: { rate: ev.rate } }, 2);
|
|
1648
|
+
break;
|
|
1649
|
+
case "leak": {
|
|
1650
|
+
const owner = meta(ev.container).objectId;
|
|
1651
|
+
if (owner && c.now() - (leakAt.get(owner) ?? -99) <= 2) break;
|
|
1652
|
+
c.log.coalesce(`fleak:${ev.container}`, { channel: "event", type: f + "leak", ...meta(ev.container), text: t("fl.leak", { who: who(ev.container), liquid: c.liquidName(ev.liquid), rate: num(ev.rate, ev.rate < 0.01 ? 3 : 2) }), data: { rate: ev.rate, point: ev.point } }, 2);
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
case "drink": {
|
|
1656
|
+
const now = c.now();
|
|
1657
|
+
let acc = drinkAcc.get(ev.container);
|
|
1658
|
+
if (!acc || now - acc.last > 0.5) acc = { liters: 0, thirst: 0, burn: false, effects: /* @__PURE__ */ new Set(), last: now, start: now };
|
|
1659
|
+
acc.liters += ev.liters;
|
|
1660
|
+
acc.thirst += ev.thirst;
|
|
1661
|
+
acc.burn = acc.burn || ev.burn;
|
|
1662
|
+
for (const e of ev.effects) acc.effects.add(e);
|
|
1663
|
+
acc.last = now;
|
|
1664
|
+
drinkAcc.set(ev.container, acc);
|
|
1665
|
+
const effects = [...acc.effects].map((e) => t(`effect.${e}`)).join(", ");
|
|
1666
|
+
c.log.coalesce(
|
|
1667
|
+
`drink:${ev.container}:${acc.start}`,
|
|
1668
|
+
{
|
|
1669
|
+
channel: "event",
|
|
1670
|
+
type: f + "drink",
|
|
1671
|
+
...meta(ev.container),
|
|
1672
|
+
text: t("fl.drink", { who: who(ev.container), liters: num(acc.liters, 2), thirst: num(acc.thirst, 0) }) + (effects ? `, ${effects}` : "") + (acc.burn ? `, ${t("fl.burn")}` : ""),
|
|
1673
|
+
data: { liters: acc.liters, temperature: ev.temperature, thirst: acc.thirst, effects: [...acc.effects], burn: acc.burn }
|
|
1674
|
+
},
|
|
1675
|
+
0.5
|
|
1676
|
+
);
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
case "empty":
|
|
1680
|
+
c.log.add({ channel: "event", type: f + "empty", ...meta(ev.container), text: t("fl.empty", { who: who(ev.container) }) });
|
|
1681
|
+
break;
|
|
1682
|
+
case "splash":
|
|
1683
|
+
c.log.coalesce(`splash:${ev.liquid}`, { channel: "event", type: f + "splash", text: t("fl.splash", { liquid: c.liquidName(ev.liquid) }), data: { point: ev.point, intensity: ev.intensity } }, 1);
|
|
1684
|
+
break;
|
|
1685
|
+
case "stain": {
|
|
1686
|
+
const surface = t(`surface.${ev.surface}`);
|
|
1687
|
+
const liquid = c.liquidName(ev.liquid);
|
|
1688
|
+
stainInfo.set(ev.stain, { liquid: ev.liquid, surface, x: ev.point[0], z: ev.point[2] });
|
|
1689
|
+
const key = `stain:${ev.liquid}:${ev.surface}`;
|
|
1690
|
+
const acc = stainAcc.get(key);
|
|
1691
|
+
const n = acc && c.now() - acc.last <= 1 ? acc.n + 1 : 1;
|
|
1692
|
+
stainAcc.set(key, { n, last: c.now() });
|
|
1693
|
+
c.log.coalesce(
|
|
1694
|
+
key,
|
|
1695
|
+
{
|
|
1696
|
+
channel: "event",
|
|
1697
|
+
type: f + "stain",
|
|
1698
|
+
text: n > 1 ? t("fl.stains", { n, liquid, surface, where: xz(ev.point[0], ev.point[2]) }) : t("fl.stain", { liquid, surface, where: xz(ev.point[0], ev.point[2]) }),
|
|
1699
|
+
data: { stain: ev.stain, surface: ev.surface, liquid: ev.liquid, point: ev.point, count: n }
|
|
1700
|
+
},
|
|
1701
|
+
1
|
|
1702
|
+
);
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
case "dried": {
|
|
1706
|
+
const info = stainInfo.get(ev.stain);
|
|
1707
|
+
const key = `dried:${info?.liquid ?? "?"}:${ev.mark}`;
|
|
1708
|
+
const acc = driedAcc.get(key);
|
|
1709
|
+
const n = acc && c.now() - acc.last <= 1.5 ? acc.n + 1 : 1;
|
|
1710
|
+
driedAcc.set(key, { n, last: c.now() });
|
|
1711
|
+
const vars = { liquid: info ? c.liquidName(info.liquid) : "", surface: info?.surface ?? "", where: info ? xz(info.x, info.z) : "", n };
|
|
1712
|
+
const text = !info ? t(ev.mark ? "fl.dried.mark" : "fl.dried") : n > 1 ? t(ev.mark ? "fl.dried.many.mark" : "fl.dried.many", vars) : t(ev.mark ? "fl.dried.known.mark" : "fl.dried.known", vars);
|
|
1713
|
+
c.log.coalesce(key, { channel: "event", type: f + "dried", text, data: { stain: ev.stain, mark: ev.mark, count: n } }, 1.5);
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
case "mix":
|
|
1717
|
+
c.log.add({ channel: "event", type: f + "mix", ...meta(ev.container), text: t("fl.mix", { who: who(ev.container), from: c.liquidName(ev.from), into: c.liquidName(ev.into) }) });
|
|
1718
|
+
break;
|
|
1719
|
+
default:
|
|
1720
|
+
break;
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
return { events, fluid };
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/player.ts
|
|
1727
|
+
var EYE = 1.7;
|
|
1728
|
+
var RADIUS = 0.3;
|
|
1729
|
+
var WALK = 3.4;
|
|
1730
|
+
var RUN = 6.2;
|
|
1731
|
+
var JUMP = 4.6;
|
|
1732
|
+
var GRAVITY = 13;
|
|
1733
|
+
var LOOK = 22e-4;
|
|
1734
|
+
function createPlayer(THREE, o) {
|
|
1735
|
+
const cam = o.camera;
|
|
1736
|
+
let yaw = 0;
|
|
1737
|
+
let pitch = 0;
|
|
1738
|
+
let y = 0;
|
|
1739
|
+
let eye = EYE;
|
|
1740
|
+
let vy = 0;
|
|
1741
|
+
const px = { x: o.start[0], z: o.start[2] };
|
|
1742
|
+
let locked = false;
|
|
1743
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1744
|
+
const euler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
1745
|
+
const apply = () => {
|
|
1746
|
+
euler.set(pitch, yaw, 0, "YXZ");
|
|
1747
|
+
cam.quaternion.setFromEuler(euler);
|
|
1748
|
+
cam.position.set(px.x, eye + y, px.z);
|
|
1749
|
+
cam.updateMatrixWorld(true);
|
|
1750
|
+
};
|
|
1751
|
+
apply();
|
|
1752
|
+
const blocked = (x, z) => {
|
|
1753
|
+
if (Math.abs(x) > o.limit - RADIUS || Math.abs(z) > o.limit - RADIUS) return true;
|
|
1754
|
+
for (const c of o.colliders) if (x > c[0] - RADIUS && x < c[2] + RADIUS && z > c[1] - RADIUS && z < c[3] + RADIUS) return true;
|
|
1755
|
+
return false;
|
|
1756
|
+
};
|
|
1757
|
+
const move = (dx, dz) => {
|
|
1758
|
+
if (!blocked(px.x + dx, px.z)) px.x += dx;
|
|
1759
|
+
if (!blocked(px.x, px.z + dz)) px.z += dz;
|
|
1760
|
+
};
|
|
1761
|
+
let dragging = false;
|
|
1762
|
+
let moved = 0;
|
|
1763
|
+
let downAt = 0;
|
|
1764
|
+
let useStarted = false;
|
|
1765
|
+
let holdTimer = null;
|
|
1766
|
+
const look = (dx, dy) => {
|
|
1767
|
+
yaw -= dx * LOOK;
|
|
1768
|
+
pitch = Math.max(-1.5, Math.min(1.5, pitch - dy * LOOK));
|
|
1769
|
+
};
|
|
1770
|
+
const onLockChange = () => {
|
|
1771
|
+
locked = document.pointerLockElement === o.dom;
|
|
1772
|
+
};
|
|
1773
|
+
const onMouseDown = (e) => {
|
|
1774
|
+
o.dom.focus?.();
|
|
1775
|
+
if (e.button === 2) {
|
|
1776
|
+
e.preventDefault();
|
|
1777
|
+
o.hooks.secondary();
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (e.button !== 0) return;
|
|
1781
|
+
if (!locked) {
|
|
1782
|
+
try {
|
|
1783
|
+
const r = o.dom.requestPointerLock?.call(o.dom);
|
|
1784
|
+
if (r && typeof r.catch === "function") r.catch(() => void 0);
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
dragging = true;
|
|
1788
|
+
moved = 0;
|
|
1789
|
+
downAt = performance.now();
|
|
1790
|
+
useStarted = false;
|
|
1791
|
+
holdTimer = setTimeout(() => {
|
|
1792
|
+
if (dragging && moved < 4 && !useStarted) {
|
|
1793
|
+
useStarted = true;
|
|
1794
|
+
o.hooks.primaryDown();
|
|
1795
|
+
}
|
|
1796
|
+
}, 220);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
useStarted = true;
|
|
1800
|
+
o.hooks.primaryDown();
|
|
1801
|
+
};
|
|
1802
|
+
const onMouseUp = (e) => {
|
|
1803
|
+
if (e.button !== 0) return;
|
|
1804
|
+
if (holdTimer) clearTimeout(holdTimer);
|
|
1805
|
+
if (dragging) {
|
|
1806
|
+
dragging = false;
|
|
1807
|
+
if (!useStarted && moved < 4 && performance.now() - downAt < 400) {
|
|
1808
|
+
o.hooks.primaryDown();
|
|
1809
|
+
o.hooks.primaryUp();
|
|
1810
|
+
} else if (useStarted) o.hooks.primaryUp();
|
|
1811
|
+
useStarted = false;
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (useStarted) o.hooks.primaryUp();
|
|
1815
|
+
useStarted = false;
|
|
1816
|
+
};
|
|
1817
|
+
const onMouseMove = (e) => {
|
|
1818
|
+
if (locked) look(e.movementX, e.movementY);
|
|
1819
|
+
else if (dragging) {
|
|
1820
|
+
moved += Math.abs(e.movementX) + Math.abs(e.movementY);
|
|
1821
|
+
if (moved >= 4) {
|
|
1822
|
+
if (useStarted) {
|
|
1823
|
+
useStarted = false;
|
|
1824
|
+
o.hooks.primaryUp();
|
|
1825
|
+
}
|
|
1826
|
+
look(e.movementX, e.movementY);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
const onWheel = (e) => {
|
|
1831
|
+
e.preventDefault();
|
|
1832
|
+
o.hooks.wheel(e.deltaY > 0 ? 1 : -1);
|
|
1833
|
+
};
|
|
1834
|
+
const typing = (e) => {
|
|
1835
|
+
const t = e.target;
|
|
1836
|
+
return !!t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA");
|
|
1837
|
+
};
|
|
1838
|
+
const onKeyDown = (e) => {
|
|
1839
|
+
if (typing(e)) return;
|
|
1840
|
+
if (e.code === "Space" || e.code === "Tab") e.preventDefault();
|
|
1841
|
+
if (!e.repeat) keys.add(e.code);
|
|
1842
|
+
o.hooks.key(e.code, true, e);
|
|
1843
|
+
};
|
|
1844
|
+
const onKeyUp = (e) => {
|
|
1845
|
+
keys.delete(e.code);
|
|
1846
|
+
if (typing(e)) return;
|
|
1847
|
+
o.hooks.key(e.code, false, e);
|
|
1848
|
+
};
|
|
1849
|
+
const onContext = (e) => e.preventDefault();
|
|
1850
|
+
const onBlur = () => keys.clear();
|
|
1851
|
+
o.dom.addEventListener("mousedown", onMouseDown);
|
|
1852
|
+
window.addEventListener("mouseup", onMouseUp);
|
|
1853
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
1854
|
+
o.dom.addEventListener("wheel", onWheel, { passive: false });
|
|
1855
|
+
o.dom.addEventListener("contextmenu", onContext);
|
|
1856
|
+
document.addEventListener("pointerlockchange", onLockChange);
|
|
1857
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1858
|
+
window.addEventListener("keyup", onKeyUp);
|
|
1859
|
+
window.addEventListener("blur", onBlur);
|
|
1860
|
+
return {
|
|
1861
|
+
get yaw() {
|
|
1862
|
+
return yaw;
|
|
1863
|
+
},
|
|
1864
|
+
get pitch() {
|
|
1865
|
+
return pitch;
|
|
1866
|
+
},
|
|
1867
|
+
get locked() {
|
|
1868
|
+
return locked;
|
|
1869
|
+
},
|
|
1870
|
+
keys,
|
|
1871
|
+
update(dt) {
|
|
1872
|
+
const run = keys.has("ShiftLeft") || keys.has("ShiftRight");
|
|
1873
|
+
const speed = run ? RUN : WALK;
|
|
1874
|
+
let f = 0;
|
|
1875
|
+
let r = 0;
|
|
1876
|
+
if (keys.has("KeyW") || keys.has("ArrowUp")) f += 1;
|
|
1877
|
+
if (keys.has("KeyS") || keys.has("ArrowDown")) f -= 1;
|
|
1878
|
+
if (keys.has("KeyD") || keys.has("ArrowRight")) r += 1;
|
|
1879
|
+
if (keys.has("KeyA") || keys.has("ArrowLeft")) r -= 1;
|
|
1880
|
+
if ((f || r || keys.has("Space")) && eye !== EYE) eye = EYE;
|
|
1881
|
+
if (f || r) {
|
|
1882
|
+
const len = Math.hypot(f, r);
|
|
1883
|
+
const fx = -Math.sin(yaw);
|
|
1884
|
+
const fz = -Math.cos(yaw);
|
|
1885
|
+
const rx = Math.cos(yaw);
|
|
1886
|
+
const rz = -Math.sin(yaw);
|
|
1887
|
+
move((fx * f + rx * r) / len * speed * dt, (fz * f + rz * r) / len * speed * dt);
|
|
1888
|
+
}
|
|
1889
|
+
if (y <= 0 && keys.has("Space")) vy = JUMP;
|
|
1890
|
+
if (y > 0 || vy > 0) {
|
|
1891
|
+
vy -= GRAVITY * dt;
|
|
1892
|
+
y += vy * dt;
|
|
1893
|
+
if (y <= 0) {
|
|
1894
|
+
y = 0;
|
|
1895
|
+
vy = 0;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
apply();
|
|
1899
|
+
},
|
|
1900
|
+
setPose(position, yw, pt) {
|
|
1901
|
+
px.x = position[0];
|
|
1902
|
+
px.z = position[2];
|
|
1903
|
+
eye = position[1];
|
|
1904
|
+
y = 0;
|
|
1905
|
+
vy = 0;
|
|
1906
|
+
yaw = yw;
|
|
1907
|
+
pitch = pt;
|
|
1908
|
+
apply();
|
|
1909
|
+
},
|
|
1910
|
+
look(position, target) {
|
|
1911
|
+
px.x = position[0];
|
|
1912
|
+
px.z = position[2];
|
|
1913
|
+
eye = position[1];
|
|
1914
|
+
y = 0;
|
|
1915
|
+
vy = 0;
|
|
1916
|
+
const dx = target[0] - position[0];
|
|
1917
|
+
const dy = target[1] - position[1];
|
|
1918
|
+
const dz = target[2] - position[2];
|
|
1919
|
+
yaw = Math.atan2(-dx, -dz);
|
|
1920
|
+
pitch = Math.atan2(dy, Math.hypot(dx, dz));
|
|
1921
|
+
apply();
|
|
1922
|
+
},
|
|
1923
|
+
kick(pitchDeg, yawDeg) {
|
|
1924
|
+
pitch = Math.max(-1.5, Math.min(1.5, pitch + pitchDeg * Math.PI / 180));
|
|
1925
|
+
yaw += yawDeg * Math.PI / 180;
|
|
1926
|
+
apply();
|
|
1927
|
+
},
|
|
1928
|
+
walk(forward, right) {
|
|
1929
|
+
move(-Math.sin(yaw) * forward + Math.cos(yaw) * right, -Math.cos(yaw) * forward - Math.sin(yaw) * right);
|
|
1930
|
+
apply();
|
|
1931
|
+
},
|
|
1932
|
+
dispose() {
|
|
1933
|
+
if (holdTimer) clearTimeout(holdTimer);
|
|
1934
|
+
o.dom.removeEventListener("mousedown", onMouseDown);
|
|
1935
|
+
window.removeEventListener("mouseup", onMouseUp);
|
|
1936
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
1937
|
+
o.dom.removeEventListener("wheel", onWheel);
|
|
1938
|
+
o.dom.removeEventListener("contextmenu", onContext);
|
|
1939
|
+
document.removeEventListener("pointerlockchange", onLockChange);
|
|
1940
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
1941
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
1942
|
+
window.removeEventListener("blur", onBlur);
|
|
1943
|
+
if (document.pointerLockElement === o.dom) document.exitPointerLock?.();
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/scenarios.ts
|
|
1949
|
+
import { JOINT_DEFAULTS } from "@adatechnology/game-destruction";
|
|
1950
|
+
function makeScenarios(t, num, _locale = "en") {
|
|
1951
|
+
const yn = (b) => t(b ? "yes" : "no");
|
|
1952
|
+
const cn = (what, n) => t(`count.${what}.${n === 1 ? "one" : "other"}`, { n });
|
|
1953
|
+
const pc = (n) => t(n === 1 ? "count.piece.one" : "count.piece.other", { n });
|
|
1954
|
+
const sc = (id, run) => ({ id, name: t(`sc.${id}`), run });
|
|
1955
|
+
const at = (room, itemId, x, z, y = 0, rot = 0) => room.spawn(itemId, [x, y, z], rot).objectId;
|
|
1956
|
+
const look = (room, from, to) => room.look(from, to);
|
|
1957
|
+
const part = (room, id, p) => room.world.part(id, p);
|
|
1958
|
+
const count = (room, type, objectId) => room.log.entries({ type, objectId }).length;
|
|
1959
|
+
const anyBurning = (room, id) => (room.world.state(id)?.parts ?? []).some((p) => p.burning);
|
|
1960
|
+
const igniteHit = (room, id, point, temperature = 900, seconds = 3) => {
|
|
1961
|
+
room.world.hit(id, { type: "fire", point, temperature, radius: 0.6, duration: seconds });
|
|
1962
|
+
};
|
|
1963
|
+
const COLUMN = { column_0_0_0: [-2, 1.4, -8], column_0_1_0: [2, 1.4, -8], column_0_0_1: [-2, 1.4, -4], column_0_1_1: [2, 1.4, -4] };
|
|
1964
|
+
const killColumns = (room, id, ids) => {
|
|
1965
|
+
for (const c of ids) room.world.hitPart(id, c, { type: "blunt", point: COLUMN[c], energy: 3e6, radius: 0.05, source: "test" });
|
|
1966
|
+
};
|
|
1967
|
+
const heavyCollapse = (room) => room.log.entries({ type: "collapse" }).reduce((m, e) => Math.max(m, Number(e.data?.massKg ?? 0)), 0);
|
|
1968
|
+
return [
|
|
1969
|
+
sc("steel_through_hole", ({ room, step, expect }) => {
|
|
1970
|
+
const id = at(room, "steel_sheet", 0, -4);
|
|
1971
|
+
room.setTool("pistol_9mm");
|
|
1972
|
+
look(room, [0, 1.1, -1.5], [0, 1.1, -4]);
|
|
1973
|
+
room.use();
|
|
1974
|
+
step(0.2);
|
|
1975
|
+
const p = part(room, id, "sheet");
|
|
1976
|
+
const hole = p?.holes[0];
|
|
1977
|
+
expect(
|
|
1978
|
+
!!hole?.through,
|
|
1979
|
+
t("sc.steel_through_hole.check", { mm: num((hole?.diameter ?? 0) * 1e3, 0), through: yn(!!hole?.through), holes: p?.holes.length ?? 0, hp: `${num(p?.hp ?? 0, 0)}/${num(p?.maxHp ?? 0, 0)}` }),
|
|
1980
|
+
{ holes: p?.holes.length, through: hole?.through }
|
|
1981
|
+
);
|
|
1982
|
+
}),
|
|
1983
|
+
sc("concrete_stops_bullet", ({ room, step, expect }) => {
|
|
1984
|
+
const id = at(room, "concrete_block", 0, -4);
|
|
1985
|
+
room.setTool("pistol_9mm");
|
|
1986
|
+
look(room, [0, 0.5, -1.5], [0, 0.2, -3.8]);
|
|
1987
|
+
room.use();
|
|
1988
|
+
step(0.2);
|
|
1989
|
+
const p = part(room, id, "block");
|
|
1990
|
+
const wall = part(room, "target_wall", "brick_1_4");
|
|
1991
|
+
const through = p?.holes.some((h) => h.through) ?? true;
|
|
1992
|
+
expect(
|
|
1993
|
+
!through && (p?.holes.length ?? 0) + (p?.dents.length ?? 0) + (p?.cracks.length ?? 0) > 0 && (wall?.holes.length ?? 0) === 0,
|
|
1994
|
+
t("sc.concrete_stops_bullet.check", { through: yn(through), dents: cn("dent", p?.dents.length ?? 0), holes: cn("hole", p?.holes.length ?? 0), cracks: cn("crack", p?.cracks.length ?? 0), hp: `${num(p?.hp ?? 0, 0)}/${num(p?.maxHp ?? 0, 0)}` }),
|
|
1995
|
+
{ through }
|
|
1996
|
+
);
|
|
1997
|
+
}),
|
|
1998
|
+
sc("hammer_dents_drum", ({ room, step, expect }) => {
|
|
1999
|
+
const id = at(room, "steel_drum", 0, -4);
|
|
2000
|
+
room.setTool("hammer");
|
|
2001
|
+
look(room, [0, 0.6, -3.35], [0, 0.5, -4]);
|
|
2002
|
+
let swings = 0;
|
|
2003
|
+
for (; swings < 8 && (part(room, id, "drum_body")?.dents.length ?? 0) === 0; swings++) {
|
|
2004
|
+
room.use();
|
|
2005
|
+
step(1);
|
|
2006
|
+
}
|
|
2007
|
+
const p = part(room, id, "drum_body");
|
|
2008
|
+
expect(
|
|
2009
|
+
(p?.dents.length ?? 0) > 0 && p?.status !== "broken",
|
|
2010
|
+
t("sc.hammer_dents_drum.check", { swings, dents: p?.dents.length ?? 0, depth: num((p?.dents[0]?.depth ?? 0) * 1e3, 1), hp: `${num(p?.hp ?? 0, 0)}/${num(p?.maxHp ?? 0, 0)}` }),
|
|
2011
|
+
{ swings }
|
|
2012
|
+
);
|
|
2013
|
+
}),
|
|
2014
|
+
sc("glass_shatters", ({ room, step, expect }) => {
|
|
2015
|
+
const id = at(room, "glass_pane", 0, -4);
|
|
2016
|
+
room.setTool("pistol_9mm");
|
|
2017
|
+
look(room, [0, 1.3, -1.5], [0, 1.05, -4]);
|
|
2018
|
+
let shots = 0;
|
|
2019
|
+
for (; shots < 10 && part(room, id, "pane")?.status !== "broken"; shots++) {
|
|
2020
|
+
room.use();
|
|
2021
|
+
step(0.25);
|
|
2022
|
+
}
|
|
2023
|
+
step(0.5);
|
|
2024
|
+
const fr = room.log.entries({ type: "fracture", objectId: id }).find((e) => e.part === "pane");
|
|
2025
|
+
const n = Number(fr?.data?.fragments ?? 0);
|
|
2026
|
+
expect(part(room, id, "pane")?.status === "broken" && n >= 4, t("sc.glass_shatters.check", { shots, n }), { shots, fragments: n });
|
|
2027
|
+
}),
|
|
2028
|
+
sc("shotgun_splinters_crate", ({ room, step, expect }) => {
|
|
2029
|
+
const id = at(room, "wooden_crate", 0, -4);
|
|
2030
|
+
room.setTool("shotgun_pump_12");
|
|
2031
|
+
look(room, [0, 0.6, -1.8], [0, 0.25, -3.77]);
|
|
2032
|
+
let shots = 0;
|
|
2033
|
+
const splinters = () => room.log.entries({ type: "fracture", objectId: id }).filter((e) => e.data?.style === "splinter").length;
|
|
2034
|
+
for (; shots < 4 && splinters() === 0; shots++) {
|
|
2035
|
+
room.use();
|
|
2036
|
+
step(1.1);
|
|
2037
|
+
}
|
|
2038
|
+
const holes = (room.world.state(id)?.parts ?? []).reduce((n, p) => n + p.holes.length, 0);
|
|
2039
|
+
expect(splinters() > 0, t("sc.shotgun_splinters_crate.check", { shots, splinters: splinters(), holes }), { shots });
|
|
2040
|
+
}),
|
|
2041
|
+
sc("kick_chair_leg", ({ room, step, expect }) => {
|
|
2042
|
+
const id = at(room, "chair", 0, -4);
|
|
2043
|
+
const before = room.renderer.pieces().length;
|
|
2044
|
+
const leg = part(room, id, "leg_1");
|
|
2045
|
+
void leg;
|
|
2046
|
+
let kicks = 0;
|
|
2047
|
+
for (; kicks < 6 && room.renderer.pieces().length <= before && part(room, id, "leg_1"); kicks++) {
|
|
2048
|
+
room.world.hitPart(id, "leg_1", { type: "blunt", point: [-0.18, 0.2, -4.18], direction: [1, 0, 0], energy: 220, radius: 0.05, source: "kick" });
|
|
2049
|
+
step(0.3);
|
|
2050
|
+
}
|
|
2051
|
+
step(0.5);
|
|
2052
|
+
const after = room.renderer.pieces().length;
|
|
2053
|
+
const groups = room.world.state(id)?.groups.length ?? 1;
|
|
2054
|
+
expect(
|
|
2055
|
+
after > before,
|
|
2056
|
+
t("sc.kick_chair_leg.check", { kicks, groups, before, after, leg: part(room, id, "leg_1") ? t("leg.still") : t("leg.gone") }),
|
|
2057
|
+
{ kicks, before, after }
|
|
2058
|
+
);
|
|
2059
|
+
}),
|
|
2060
|
+
sc("screwdriver_shelf_board", ({ room, step, expect }) => {
|
|
2061
|
+
const id = at(room, "shelf", 0, -4);
|
|
2062
|
+
room.setTool("screwdriver");
|
|
2063
|
+
look(room, [0, 0.8, -3.5], [0, 0.75, -3.86]);
|
|
2064
|
+
const t0 = room.log.entries().length ? room.log.entries().slice(-1)[0].t : 0;
|
|
2065
|
+
room.use(5);
|
|
2066
|
+
const done = room.log.entries({ type: "detach", objectId: id })[0];
|
|
2067
|
+
const elapsed = done ? done.t - t0 : 5;
|
|
2068
|
+
const want = JOINT_DEFAULTS.screw.seconds;
|
|
2069
|
+
const groups = room.renderer.pieces().length;
|
|
2070
|
+
expect(!!done && Math.abs(elapsed - want) < 0.6 && groups >= 1, t("sc.screwdriver_shelf_board.check", { secs: num(elapsed, 1), want: num(want, 1), pieces: pc(groups) }), { elapsed, want });
|
|
2071
|
+
step(0.2);
|
|
2072
|
+
}),
|
|
2073
|
+
sc("wrong_tool_refused", ({ room, expect }) => {
|
|
2074
|
+
const id = at(room, "steel_sheet", 0, -4);
|
|
2075
|
+
room.setTool("screwdriver");
|
|
2076
|
+
look(room, [0.2, 1.05, -3.6], [0.34, 1, -4]);
|
|
2077
|
+
room.use(0.5);
|
|
2078
|
+
const refused = count(room, "tool_refused");
|
|
2079
|
+
const opened = (room.world.state(id)?.joints ?? []).filter((j) => j.open).length;
|
|
2080
|
+
expect(refused > 0 && opened === 0, t("sc.wrong_tool_refused.check", { refused, opened }), { refused, opened });
|
|
2081
|
+
}),
|
|
2082
|
+
sc("lighter_burns_crate", ({ room, step, expect }) => {
|
|
2083
|
+
const id = at(room, "wooden_crate", 0, -4);
|
|
2084
|
+
room.setTool("lighter");
|
|
2085
|
+
look(room, [0, 0.3, -3.65], [0, 0.25, -3.76]);
|
|
2086
|
+
room.use(5);
|
|
2087
|
+
const lit = anyBurning(room, id);
|
|
2088
|
+
expect(lit, t("sc.lighter_burns_crate.lit", { lit: yn(lit) }));
|
|
2089
|
+
step(500);
|
|
2090
|
+
const parts = room.world.state(id)?.parts ?? [];
|
|
2091
|
+
const ash = parts.filter((p) => p.status === "ashes").length;
|
|
2092
|
+
const charred = Math.max(...parts.map((p) => p.char));
|
|
2093
|
+
expect(ash === parts.length || ash > 0 && parts.every((p) => p.status === "ashes" || p.status === "broken" || p.char > 0.8), t("sc.lighter_burns_crate.ash", { ash, total: parts.length, char: num(charred * 100, 0) }), { ash, total: parts.length });
|
|
2094
|
+
}),
|
|
2095
|
+
sc("torch_melts_steel", ({ room, step, expect }) => {
|
|
2096
|
+
const drum2 = at(room, "steel_drum", 0, -4);
|
|
2097
|
+
const crate2 = at(room, "wooden_crate", 0.7, -4);
|
|
2098
|
+
igniteHit(room, crate2, [0.7, 0.25, -3.8], 900, 2);
|
|
2099
|
+
let peak = 0;
|
|
2100
|
+
for (let i = 0; i < 40; i++) {
|
|
2101
|
+
step(1);
|
|
2102
|
+
peak = Math.max(peak, part(room, drum2, "drum_body")?.temperature ?? 0);
|
|
2103
|
+
}
|
|
2104
|
+
const melt0 = part(room, drum2, "drum_body")?.melt ?? 0;
|
|
2105
|
+
expect(melt0 === 0 && peak < 1450, t("sc.torch_melts_steel.wood", { peak: num(peak, 0), melt: num(melt0 * 100, 0), crate: yn(anyBurning(room, crate2)) }), { peak });
|
|
2106
|
+
room.setTool("cutting_torch");
|
|
2107
|
+
look(room, [-0.15, 0.55, -3.55], [-0.15, 0.5, -3.72]);
|
|
2108
|
+
room.use(20);
|
|
2109
|
+
const p = part(room, drum2, "drum_body");
|
|
2110
|
+
expect((p?.melt ?? 0) > 0.02 || p?.status === "melted" || (p?.holes.length ?? 0) > 0, t("sc.torch_melts_steel.torch", { temp: num(p?.temperature ?? 0, 0), melt: num((p?.melt ?? 0) * 100, 0) }), { temperature: p?.temperature, melt: p?.melt });
|
|
2111
|
+
}),
|
|
2112
|
+
sc("bucket_melts_drips", ({ room, step, expect }) => {
|
|
2113
|
+
const id = at(room, "plastic_bucket", 0, -4);
|
|
2114
|
+
room.setTool("flamethrower");
|
|
2115
|
+
look(room, [0, 0.5, -2.4], [0, 0.15, -4]);
|
|
2116
|
+
room.use(2.5);
|
|
2117
|
+
step(10);
|
|
2118
|
+
const p = part(room, id, "bucket_body");
|
|
2119
|
+
const drips = count(room, "drip", id);
|
|
2120
|
+
expect((p?.melt ?? 0) > 0.05 || p?.status === "melted", t("sc.bucket_melts_drips.check", { melt: num((p?.melt ?? 0) * 100, 0), drips, temp: num(p?.temperature ?? 0, 0) }), { melt: p?.melt, drips });
|
|
2121
|
+
}),
|
|
2122
|
+
sc("gas_bottle_explodes", ({ room, step, expect }) => {
|
|
2123
|
+
const bottle2 = at(room, "gas_bottle", 0, -4);
|
|
2124
|
+
const crate2 = at(room, "wooden_crate", 1.3, -4);
|
|
2125
|
+
const c0 = room.spawned().find((s) => s.objectId === crate2)?.root.position.clone();
|
|
2126
|
+
let seconds = 0;
|
|
2127
|
+
for (; seconds < 60 && count(room, "explode", bottle2) === 0; seconds++) {
|
|
2128
|
+
igniteHit(room, bottle2, [0, 0.4, -3.9], 1e3, 1);
|
|
2129
|
+
step(1);
|
|
2130
|
+
}
|
|
2131
|
+
step(1.5);
|
|
2132
|
+
const c1 = room.spawned().find((s) => s.objectId === crate2)?.root.position;
|
|
2133
|
+
const moved = c0 && c1 ? c0.distanceTo(c1) : 0;
|
|
2134
|
+
const state = room.world.state(crate2);
|
|
2135
|
+
const lost = (state?.parts ?? []).filter((p) => p.status === "broken").length + (state && state.groups.length > 1 ? 1 : 0);
|
|
2136
|
+
expect(count(room, "explode", bottle2) > 0, t("sc.gas_bottle_explodes.explode", { seconds }), { seconds });
|
|
2137
|
+
expect(moved > 0.3 || lost > 0, t("sc.gas_bottle_explodes.push", { moved: num(moved, 1), lost: pc(lost) }), { moved, lost });
|
|
2138
|
+
}),
|
|
2139
|
+
sc("bottle_drop_shatters", ({ room, step, expect }) => {
|
|
2140
|
+
const id = at(room, "glass_bottle", 0, -3, 1.5);
|
|
2141
|
+
step(2);
|
|
2142
|
+
const p = part(room, id, "bottle_body");
|
|
2143
|
+
const stain = room.fluids.stains().find((s) => s.liquid === "water");
|
|
2144
|
+
expect(p?.status === "broken", t("sc.bottle_drop_shatters.check", { status: p?.status ?? "?", cracks: p?.cracks.length ?? 0, stain: yn(!!stain) }), { status: p?.status });
|
|
2145
|
+
}),
|
|
2146
|
+
sc("throw_chair_at_wall", ({ room, step, expect }) => {
|
|
2147
|
+
const id = at(room, "chair", 0, -5.2);
|
|
2148
|
+
look(room, [0, 1.7, -4.2], [0, 0.35, -5.2]);
|
|
2149
|
+
room.grab();
|
|
2150
|
+
look(room, [0, 1.7, -4.2], [0, 2.3, -11]);
|
|
2151
|
+
step(0.3);
|
|
2152
|
+
room.throwHeld(16);
|
|
2153
|
+
step(2.5);
|
|
2154
|
+
const hits = room.log.entries({ type: "impact" });
|
|
2155
|
+
const wallDamage = room.log.entries().filter((e) => e.objectId === "target_wall" && (e.type === "dent" || e.type === "damage" || e.type === "crack" || e.type === "fracture")).length;
|
|
2156
|
+
expect(hits.length > 0 && wallDamage > 0, t("sc.throw_chair_at_wall.check", { impacts: hits.length, wall: wallDamage, energy: num(Number(hits[0]?.data?.energyJ ?? 0), 0) }), { id });
|
|
2157
|
+
}),
|
|
2158
|
+
sc("coffee_three_sips", ({ room, step, expect }) => {
|
|
2159
|
+
const id = at(room, "coffee_cup", 0, -3);
|
|
2160
|
+
look(room, [0, 1, -1.8], [0, 0.05, -3]);
|
|
2161
|
+
const cid = `${id}_fluid`;
|
|
2162
|
+
const before = room.fluids.container(cid)?.liters ?? 0;
|
|
2163
|
+
room.grab();
|
|
2164
|
+
for (let i = 0; i < 3; i++) {
|
|
2165
|
+
room.useItem("sip");
|
|
2166
|
+
step(1.4);
|
|
2167
|
+
}
|
|
2168
|
+
const after = room.fluids.container(cid)?.liters ?? 0;
|
|
2169
|
+
const drinks = room.log.entries().filter((e) => e.type === "fluid.drink");
|
|
2170
|
+
const caffeine = drinks.some((e) => Array.isArray(e.data?.effects) && (e.data?.effects).includes("caffeine"));
|
|
2171
|
+
expect(drinks.length === 3 && before - after > 0.06 && caffeine, t("sc.coffee_three_sips.check", { sips: drinks.length, before: num(before, 2), after: num(after, 2), caffeine: yn(caffeine), burn: yn(drinks.some((e) => !!e.data?.burn)) }), { before, after });
|
|
2172
|
+
}),
|
|
2173
|
+
sc("tilt_cup_spills", ({ room, step, expect }) => {
|
|
2174
|
+
const id = at(room, "coffee_cup", 0, -3);
|
|
2175
|
+
look(room, [0, 1, -1.8], [0, 0.05, -3]);
|
|
2176
|
+
const cid = `${id}_fluid`;
|
|
2177
|
+
const before = room.fluids.container(cid)?.liters ?? 0;
|
|
2178
|
+
room.grab();
|
|
2179
|
+
step(0.6);
|
|
2180
|
+
room.tilt(2);
|
|
2181
|
+
step(2.5);
|
|
2182
|
+
const after = room.fluids.container(cid)?.liters ?? 0;
|
|
2183
|
+
const stains = room.fluids.stains().filter((s) => s.liquid === "coffee").length;
|
|
2184
|
+
expect(before - after > 0.03 && stains > 0, t("sc.tilt_cup_spills.check", { before: num(before, 2), after: num(after, 2), stains }), { before, after });
|
|
2185
|
+
}),
|
|
2186
|
+
sc("jerrycan_leaks_fuel", ({ room, step, expect }) => {
|
|
2187
|
+
const id = at(room, "jerrycan", 0, -3.5);
|
|
2188
|
+
room.setTool("pistol_9mm");
|
|
2189
|
+
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2190
|
+
room.use();
|
|
2191
|
+
step(1.5);
|
|
2192
|
+
const streams = room.fluids.streams().filter((s) => s.cause === "leak" && s.liquid === "fuel");
|
|
2193
|
+
step(5);
|
|
2194
|
+
const stain = room.fluids.stains().find((s) => s.liquid === "fuel");
|
|
2195
|
+
const left = room.fluids.container(`${id}_fluid`)?.liters ?? 20;
|
|
2196
|
+
const hole = room.log.entries({ type: "hole", objectId: id })[0];
|
|
2197
|
+
expect(streams.length > 0 && !!stain && left < 20, t("sc.jerrycan_leaks_fuel.check", { streams: streams.length, stain: yn(!!stain), left: num(left, 2), leak: hole?.data?.leak ? yn(true) : yn(false) }), { left });
|
|
2198
|
+
}),
|
|
2199
|
+
sc("fuel_stain_burns", ({ room, step, expect }) => {
|
|
2200
|
+
const id = at(room, "jerrycan", 0, -3.5);
|
|
2201
|
+
const crate2 = at(room, "wooden_crate", 0.9, -3.2);
|
|
2202
|
+
room.setTool("pistol_9mm");
|
|
2203
|
+
look(room, [0, 0.4, -1.5], [0, 0.12, -3.43]);
|
|
2204
|
+
room.use();
|
|
2205
|
+
step(6);
|
|
2206
|
+
const stain = room.fluids.stains().find((s) => s.liquid === "fuel");
|
|
2207
|
+
expect(!!stain, t("sc.fuel_stain_burns.stain", { stain: yn(!!stain) }));
|
|
2208
|
+
if (!stain) return;
|
|
2209
|
+
room.setTool("lighter");
|
|
2210
|
+
look(room, [stain.center[0], 0.1, stain.center[2] + 0.06], [stain.center[0], 0, stain.center[2]]);
|
|
2211
|
+
room.use(1);
|
|
2212
|
+
step(4);
|
|
2213
|
+
const ignited = count(room, "stain_ignite");
|
|
2214
|
+
const fireHits = (room.world.state(id)?.parts ?? []).some((p) => p.burning || p.temperature > 100) || anyBurning(room, crate2);
|
|
2215
|
+
expect(ignited > 0, t("sc.fuel_stain_burns.ignite", { ignited, near: yn(fireHits) }), { ignited });
|
|
2216
|
+
step(30);
|
|
2217
|
+
expect(count(room, "stain_burned") > 0 || room.fluids.stains().filter((s) => s.liquid === "fuel" && s.wetness > 0.05).length === 0, t("sc.fuel_stain_burns.out", { burned: count(room, "stain_burned") }));
|
|
2218
|
+
}),
|
|
2219
|
+
sc("one_column_roof_stands", ({ room, step, expect }) => {
|
|
2220
|
+
const id = at(room, "building_frame", 0, -6);
|
|
2221
|
+
killColumns(room, id, ["column_0_0_0"]);
|
|
2222
|
+
step(40);
|
|
2223
|
+
const roof = part(room, id, "roof");
|
|
2224
|
+
const neighbour = part(room, id, "column_0_0_1")?.stress ?? 0;
|
|
2225
|
+
const heavy = Math.max(heavyCollapse(room), ...room.renderer.pieces().map((p) => p.mass));
|
|
2226
|
+
expect(!!roof && roof.status !== "broken" && heavy < 2e3, t("sc.one_column_roof_stands.check", { roof: roof ? t("roof.standing") : t("roof.fell"), heavy: num(heavy, 0), stress: num(neighbour * 100, 0) }), { heavy, neighbour });
|
|
2227
|
+
}),
|
|
2228
|
+
sc("two_columns_roof_collapses", ({ room, step, expect }) => {
|
|
2229
|
+
const id = at(room, "building_frame", 0, -6);
|
|
2230
|
+
killColumns(room, id, ["column_0_0_0", "column_0_1_0"]);
|
|
2231
|
+
const stress0 = room.world.part(id, "column_0_0_1")?.stress ?? 0;
|
|
2232
|
+
step(40);
|
|
2233
|
+
const pieces = room.renderer.pieces().filter((p) => p.mass >= 2e3);
|
|
2234
|
+
const logged = heavyCollapse(room) >= 2e3;
|
|
2235
|
+
const kg = pieces.reduce((m, p) => m + p.mass, 0);
|
|
2236
|
+
expect(!part(room, id, "roof") && pieces.length > 0, t("sc.two_columns_roof_collapses.check", { stress: num(stress0 * 100, 0), pieces: pc(pieces.length), kg: num(kg, 0), logged: yn(logged) }), { kg, logged });
|
|
2237
|
+
}),
|
|
2238
|
+
sc("extinguisher_puts_out_fire", ({ room, step, expect }) => {
|
|
2239
|
+
const id = at(room, "wooden_crate", 0, -4);
|
|
2240
|
+
igniteHit(room, id, [0, 0.25, -3.8], 900, 4);
|
|
2241
|
+
step(6);
|
|
2242
|
+
const before = anyBurning(room, id);
|
|
2243
|
+
expect(before, t("sc.extinguisher_puts_out_fire.burning", { burning: yn(before) }));
|
|
2244
|
+
room.setTool("extinguisher");
|
|
2245
|
+
look(room, [0, 0.9, -2.2], [0, 0.25, -4]);
|
|
2246
|
+
room.use(2.5);
|
|
2247
|
+
step(1);
|
|
2248
|
+
const after = anyBurning(room, id);
|
|
2249
|
+
expect(!after, t("sc.extinguisher_puts_out_fire.out", { burning: yn(after), events: count(room, "extinguish", id) }), { events: count(room, "extinguish", id) });
|
|
2250
|
+
})
|
|
2251
|
+
];
|
|
2252
|
+
}
|
|
2253
|
+
var makeT = (locale) => (key, vars) => {
|
|
2254
|
+
let s = resolveStrings(locale).text[key] ?? key;
|
|
2255
|
+
if (vars) for (const k of Object.keys(vars)) s = s.split(`{${k}}`).join(String(vars[k]));
|
|
2256
|
+
return s;
|
|
2257
|
+
};
|
|
2258
|
+
function scenariosFor(locale = "en") {
|
|
2259
|
+
const pt = locale.toLowerCase().startsWith("pt");
|
|
2260
|
+
return makeScenarios(makeT(locale), (n, d = 1) => pt ? (Math.round(n * 10 ** d) / 10 ** d).toFixed(d).replace(".", ",") : (Math.round(n * 10 ** d) / 10 ** d).toFixed(d), locale);
|
|
2261
|
+
}
|
|
2262
|
+
var SCENARIOS = scenariosFor("en");
|
|
2263
|
+
|
|
2264
|
+
// src/ui.ts
|
|
2265
|
+
var CSS = `
|
|
2266
|
+
.tr-root{position:absolute;inset:0;pointer-events:none;font:12px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#e8eaee;overflow:hidden}
|
|
2267
|
+
.tr-root *{box-sizing:border-box}
|
|
2268
|
+
.tr-root button,.tr-root select,.tr-root label{pointer-events:auto}
|
|
2269
|
+
.tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
|
|
2270
|
+
.tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
|
|
2271
|
+
.tr-cross:before{left:8px;top:0;width:2px;height:18px}.tr-cross:after{top:8px;left:0;height:2px;width:18px}
|
|
2272
|
+
.tr-cross i{position:absolute;left:6px;top:6px;width:6px;height:6px;background:#0b0c0e;border-radius:50%;z-index:1}
|
|
2273
|
+
.tr-top{position:absolute;top:8px;left:50%;transform:translateX(-50%);text-align:center;max-width:46vw}
|
|
2274
|
+
.tr-tool{display:inline-block;background:rgba(10,12,16,.72);padding:4px 12px;border-radius:6px;font-size:14px;font-weight:600}
|
|
2275
|
+
.tr-tool small{font-weight:400;opacity:.8;margin-left:8px}
|
|
2276
|
+
.tr-status{margin-top:4px;min-height:16px;color:#ffd23f;text-shadow:0 1px 2px #000}
|
|
2277
|
+
.tr-prog{margin:4px auto 0;width:260px;background:rgba(10,12,16,.72);border-radius:6px;padding:4px 8px;display:none}
|
|
2278
|
+
.tr-prog b{display:block;font-weight:500;margin-bottom:3px}
|
|
2279
|
+
.tr-bar{height:7px;background:#2a2e36;border-radius:4px;overflow:hidden}
|
|
2280
|
+
.tr-bar i{display:block;height:100%;background:#4fc3f7;width:0}
|
|
2281
|
+
.tr-bar.hp i{background:#66bb6a}.tr-bar.warn i{background:#ffb300}.tr-bar.bad i{background:#ef5350}
|
|
2282
|
+
.tr-bottom{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);text-align:center;max-width:56vw}
|
|
2283
|
+
.tr-held{margin-bottom:6px;background:rgba(10,12,16,.72);padding:4px 10px;border-radius:6px;display:none}
|
|
2284
|
+
.tr-held kbd,.tr-help kbd{background:#2a2e36;border:1px solid #4a505c;border-radius:3px;padding:0 4px;font:inherit;font-weight:700}
|
|
2285
|
+
.tr-hotbar{display:inline-flex;gap:3px;flex-wrap:wrap;justify-content:center}
|
|
2286
|
+
.tr-slot{pointer-events:auto;min-width:64px;max-width:92px;padding:3px 5px;background:rgba(10,12,16,.68);border:1px solid #3a404c;border-radius:5px;color:#cfd3da;cursor:pointer;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2287
|
+
.tr-slot b{color:#ffd23f;margin-right:3px}
|
|
2288
|
+
.tr-slot.on{border-color:#ffd23f;color:#fff;background:rgba(60,50,10,.8)}
|
|
2289
|
+
.tr-help{position:absolute;left:8px;bottom:8px;max-width:360px;opacity:.75;font-size:11px;background:rgba(10,12,16,.55);padding:4px 8px;border-radius:6px}
|
|
2290
|
+
.tr-left{position:absolute;top:8px;left:8px;bottom:70px;width:min(400px,36vw);display:flex;flex-direction:column;gap:8px}
|
|
2291
|
+
.tr-right{position:absolute;top:8px;right:8px;bottom:8px;width:min(310px,34vw);display:flex;flex-direction:column;gap:8px}
|
|
2292
|
+
.tr-panel{background:rgba(12,14,19,.82);border:1px solid #2c313b;border-radius:8px;padding:8px;pointer-events:auto;overflow:auto}
|
|
2293
|
+
.tr-panel h3{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:#9aa3b2;font-weight:600}
|
|
2294
|
+
.tr-insp{flex:0 0 auto;max-height:46%;min-height:60px}
|
|
2295
|
+
.tr-insp .r{display:flex;justify-content:space-between;gap:8px}.tr-insp .r span:first-child{color:#9aa3b2}
|
|
2296
|
+
.tr-insp .t{font-weight:600;font-size:13px;margin-bottom:3px}
|
|
2297
|
+
.tr-log{flex:1 1 auto;display:flex;flex-direction:column;min-height:120px;overflow:hidden}
|
|
2298
|
+
.tr-logbar{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:5px;align-items:center}
|
|
2299
|
+
.tr-logbody{flex:1;overflow:auto;font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#090a0d;border-radius:5px;padding:4px 6px}
|
|
2300
|
+
.tr-logbody div{white-space:pre-wrap;word-break:break-word;border-bottom:1px solid #15171c}
|
|
2301
|
+
.tr-logbody .action{color:#8fd3ff}.tr-logbody .event{color:#dfe3ea}.tr-logbody .check{color:#c8e6c9}.tr-logbody .check.fail{color:#ff8a80;font-weight:700}.tr-logbody .check.pass{color:#9be59f}
|
|
2302
|
+
.tr-logbody time{color:#6b7280;margin-right:6px}
|
|
2303
|
+
.tr-btn,.tr-root button.tr-btn{background:#232831;border:1px solid #3a404c;color:#e8eaee;border-radius:5px;padding:3px 8px;cursor:pointer;font:inherit}
|
|
2304
|
+
.tr-btn:hover{background:#2d3440}.tr-btn.on{background:#3b3a14;border-color:#ffd23f;color:#fff}
|
|
2305
|
+
.tr-btn.pass{border-color:#4caf50}.tr-btn.fail{border-color:#ef5350;background:#3a1d1d}
|
|
2306
|
+
.tr-tabs{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:6px}
|
|
2307
|
+
.tr-items{display:grid;grid-template-columns:1fr 1fr;gap:4px}
|
|
2308
|
+
.tr-item{display:flex;gap:2px}.tr-item .tr-btn:first-child{flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
2309
|
+
.tr-cat{flex:0 1 auto;max-height:40%}
|
|
2310
|
+
.tr-anim{flex:0 0 auto;max-height:18%}
|
|
2311
|
+
.tr-anim .list{display:flex;flex-wrap:wrap;gap:4px}.tr-anim .sub{color:#9aa3b2;margin-bottom:4px}
|
|
2312
|
+
.tr-tests{flex:1 1 auto;min-height:100px}
|
|
2313
|
+
.tr-tests .list{display:flex;flex-direction:column;gap:3px;margin-top:6px}.tr-tests .list .tr-btn{text-align:left}
|
|
2314
|
+
.tr-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;align-items:center}
|
|
2315
|
+
.tr-toggle{position:absolute;top:8px;right:8px;display:none}
|
|
2316
|
+
.tr-summary{margin-top:6px;color:#9aa3b2}
|
|
2317
|
+
@media (max-width:980px){.tr-left,.tr-right{display:none}.tr-root.open .tr-left,.tr-root.open .tr-right{display:flex}.tr-toggle{display:block;pointer-events:auto}.tr-root.open .tr-toggle{right:min(310px,34vw)}.tr-left{width:min(400px,46vw)}.tr-help{display:none}}
|
|
2318
|
+
`;
|
|
2319
|
+
var mk = (tag, cls, text) => {
|
|
2320
|
+
const e = document.createElement(tag);
|
|
2321
|
+
if (cls) e.className = cls;
|
|
2322
|
+
if (text !== void 0) e.textContent = text;
|
|
2323
|
+
return e;
|
|
2324
|
+
};
|
|
2325
|
+
function createUi(o) {
|
|
2326
|
+
const { t } = o;
|
|
2327
|
+
const style = mk("style");
|
|
2328
|
+
style.textContent = CSS;
|
|
2329
|
+
const root = mk("div", "tr-root");
|
|
2330
|
+
const pos = getComputedStyle?.(o.container)?.position;
|
|
2331
|
+
if (!pos || pos === "static") o.container.style.position = "relative";
|
|
2332
|
+
root.appendChild(style);
|
|
2333
|
+
o.container.appendChild(root);
|
|
2334
|
+
const cross = mk("div", "tr-cross");
|
|
2335
|
+
cross.appendChild(mk("i"));
|
|
2336
|
+
const top = mk("div", "tr-top");
|
|
2337
|
+
const toolBox = mk("div", "tr-tool");
|
|
2338
|
+
const status = mk("div", "tr-status");
|
|
2339
|
+
const prog = mk("div", "tr-prog");
|
|
2340
|
+
const progLabel = mk("b");
|
|
2341
|
+
const progBar = mk("div", "tr-bar");
|
|
2342
|
+
const progFill = mk("i");
|
|
2343
|
+
progBar.appendChild(progFill);
|
|
2344
|
+
prog.append(progLabel, progBar);
|
|
2345
|
+
top.append(toolBox, status, prog);
|
|
2346
|
+
const bottom = mk("div", "tr-bottom");
|
|
2347
|
+
const held = mk("div", "tr-held");
|
|
2348
|
+
const hotbar = mk("div", "tr-hotbar");
|
|
2349
|
+
bottom.append(held, hotbar);
|
|
2350
|
+
const help = mk("div", "tr-help");
|
|
2351
|
+
help.textContent = t("ui.help");
|
|
2352
|
+
const left = mk("div", "tr-left");
|
|
2353
|
+
const insp = mk("div", "tr-panel tr-insp");
|
|
2354
|
+
const logPanel = mk("div", "tr-panel tr-log");
|
|
2355
|
+
left.append(insp, logPanel);
|
|
2356
|
+
const logTitle = mk("h3", void 0, t("ui.log"));
|
|
2357
|
+
const logBar = mk("div", "tr-logbar");
|
|
2358
|
+
const logBody = mk("div", "tr-logbody");
|
|
2359
|
+
const filters = { action: true, event: true, check: true };
|
|
2360
|
+
const filterBtns = [];
|
|
2361
|
+
for (const ch of ["action", "event", "check"]) {
|
|
2362
|
+
const b = mk("button", "tr-btn on", t(`ui.filter.${ch}`));
|
|
2363
|
+
b.type = "button";
|
|
2364
|
+
b.onclick = () => {
|
|
2365
|
+
filters[ch] = !filters[ch];
|
|
2366
|
+
b.classList.toggle("on", filters[ch]);
|
|
2367
|
+
rebuildLog();
|
|
2368
|
+
};
|
|
2369
|
+
filterBtns.push(b);
|
|
2370
|
+
logBar.appendChild(b);
|
|
2371
|
+
}
|
|
2372
|
+
let autoscroll = true;
|
|
2373
|
+
const asLabel = mk("label");
|
|
2374
|
+
const asBox = mk("input");
|
|
2375
|
+
asBox.type = "checkbox";
|
|
2376
|
+
asBox.checked = true;
|
|
2377
|
+
asBox.onchange = () => {
|
|
2378
|
+
autoscroll = asBox.checked;
|
|
2379
|
+
};
|
|
2380
|
+
asLabel.append(asBox, document.createTextNode(" " + t("ui.autoscroll")));
|
|
2381
|
+
logBar.appendChild(asLabel);
|
|
2382
|
+
const logBtn = (label, fn) => {
|
|
2383
|
+
const b = mk("button", "tr-btn", label);
|
|
2384
|
+
b.type = "button";
|
|
2385
|
+
b.onclick = fn;
|
|
2386
|
+
logBar.appendChild(b);
|
|
2387
|
+
};
|
|
2388
|
+
logBtn(t("ui.copy_json"), () => {
|
|
2389
|
+
const text = o.log.toJSON();
|
|
2390
|
+
try {
|
|
2391
|
+
void navigator.clipboard.writeText(text);
|
|
2392
|
+
} catch {
|
|
2393
|
+
const ta = mk("textarea");
|
|
2394
|
+
ta.value = text;
|
|
2395
|
+
document.body.appendChild(ta);
|
|
2396
|
+
ta.select();
|
|
2397
|
+
document.execCommand?.("copy");
|
|
2398
|
+
ta.remove();
|
|
2399
|
+
}
|
|
2400
|
+
});
|
|
2401
|
+
logBtn(t("ui.download_ndjson"), () => o.log.download("ndjson"));
|
|
2402
|
+
logBtn(t("ui.download_txt"), () => o.log.download("txt"));
|
|
2403
|
+
logBtn(t("ui.clear_log"), () => {
|
|
2404
|
+
o.log.clear();
|
|
2405
|
+
logBody.textContent = "";
|
|
2406
|
+
nodes.clear();
|
|
2407
|
+
});
|
|
2408
|
+
logPanel.append(logTitle, logBar, logBody);
|
|
2409
|
+
const MAX_LINES = 200;
|
|
2410
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2411
|
+
const fmtTime = (s) => `${Math.floor(s / 60).toString().padStart(2, "0")}:${(s % 60).toFixed(1).padStart(4, "0")}`;
|
|
2412
|
+
const lineClass = (e) => `${e.channel}${e.type === "fail" || e.type === "scenario_fail" ? " fail" : e.type === "pass" || e.type === "scenario_pass" ? " pass" : ""}`;
|
|
2413
|
+
const fill = (n, e) => {
|
|
2414
|
+
n.className = lineClass(e);
|
|
2415
|
+
n.textContent = "";
|
|
2416
|
+
const tm = mk("time", void 0, fmtTime(e.t));
|
|
2417
|
+
n.append(tm, document.createTextNode(e.text));
|
|
2418
|
+
};
|
|
2419
|
+
const addNode = (e) => {
|
|
2420
|
+
if (!filters[e.channel]) return;
|
|
2421
|
+
const n = mk("div");
|
|
2422
|
+
fill(n, e);
|
|
2423
|
+
nodes.set(e.seq, n);
|
|
2424
|
+
logBody.appendChild(n);
|
|
2425
|
+
while (logBody.childElementCount > MAX_LINES) {
|
|
2426
|
+
const first = logBody.firstElementChild;
|
|
2427
|
+
for (const [k, v] of nodes) if (v === first) nodes.delete(k);
|
|
2428
|
+
first.remove();
|
|
2429
|
+
}
|
|
2430
|
+
if (autoscroll) logBody.scrollTop = logBody.scrollHeight;
|
|
2431
|
+
};
|
|
2432
|
+
function rebuildLog() {
|
|
2433
|
+
logBody.textContent = "";
|
|
2434
|
+
nodes.clear();
|
|
2435
|
+
for (const e of o.log.entries().slice(-MAX_LINES * 3)) addNode(e);
|
|
2436
|
+
}
|
|
2437
|
+
const offLog = o.log.on((e, updated) => {
|
|
2438
|
+
if (updated) {
|
|
2439
|
+
const n = nodes.get(e.seq);
|
|
2440
|
+
if (n) fill(n, e);
|
|
2441
|
+
} else addNode(e);
|
|
2442
|
+
});
|
|
2443
|
+
const right = mk("div", "tr-right");
|
|
2444
|
+
const cat = mk("div", "tr-panel tr-cat");
|
|
2445
|
+
const anim = mk("div", "tr-panel tr-anim");
|
|
2446
|
+
const tests = mk("div", "tr-panel tr-tests");
|
|
2447
|
+
right.append(cat, anim, tests);
|
|
2448
|
+
cat.appendChild(mk("h3", void 0, t("ui.catalog")));
|
|
2449
|
+
const tabs = mk("div", "tr-tabs");
|
|
2450
|
+
const items = mk("div", "tr-items");
|
|
2451
|
+
cat.append(tabs, items);
|
|
2452
|
+
let activeGroup = "";
|
|
2453
|
+
const renderItems = () => {
|
|
2454
|
+
items.textContent = "";
|
|
2455
|
+
for (const it of o.groups().get(activeGroup) ?? []) {
|
|
2456
|
+
const wrap = mk("div", "tr-item");
|
|
2457
|
+
const b = mk("button", "tr-btn", it.name);
|
|
2458
|
+
b.type = "button";
|
|
2459
|
+
b.title = it.note ?? it.name;
|
|
2460
|
+
b.onclick = () => o.onSpawn(it.id);
|
|
2461
|
+
wrap.appendChild(b);
|
|
2462
|
+
if (it.weapon) {
|
|
2463
|
+
const w = mk("button", "tr-btn", "\u2694");
|
|
2464
|
+
w.type = "button";
|
|
2465
|
+
w.title = t("ui.equip");
|
|
2466
|
+
w.onclick = () => o.onEquip(it.id);
|
|
2467
|
+
wrap.appendChild(w);
|
|
2468
|
+
}
|
|
2469
|
+
items.appendChild(wrap);
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
const renderTabs = () => {
|
|
2473
|
+
tabs.textContent = "";
|
|
2474
|
+
const groupNames = [...o.groups().keys()];
|
|
2475
|
+
if (!groupNames.includes(activeGroup)) activeGroup = groupNames[0] ?? "";
|
|
2476
|
+
for (const g of groupNames) {
|
|
2477
|
+
const b = mk("button", g === activeGroup ? "tr-btn on" : "tr-btn", g);
|
|
2478
|
+
b.type = "button";
|
|
2479
|
+
b.onclick = () => {
|
|
2480
|
+
activeGroup = g;
|
|
2481
|
+
renderTabs();
|
|
2482
|
+
};
|
|
2483
|
+
tabs.appendChild(b);
|
|
2484
|
+
}
|
|
2485
|
+
renderItems();
|
|
2486
|
+
};
|
|
2487
|
+
renderTabs();
|
|
2488
|
+
const controls = mk("div", "tr-row");
|
|
2489
|
+
const ctl = (label, fn) => {
|
|
2490
|
+
const b = mk("button", "tr-btn", label);
|
|
2491
|
+
b.type = "button";
|
|
2492
|
+
b.onclick = () => fn(b);
|
|
2493
|
+
controls.appendChild(b);
|
|
2494
|
+
return b;
|
|
2495
|
+
};
|
|
2496
|
+
ctl(t("ui.clear"), () => o.onClear());
|
|
2497
|
+
ctl(t("ui.reset_all"), () => o.onResetAll());
|
|
2498
|
+
let slow = false;
|
|
2499
|
+
ctl(t("ui.slow"), (b) => {
|
|
2500
|
+
slow = !slow;
|
|
2501
|
+
b.classList.toggle("on", slow);
|
|
2502
|
+
o.onSlow(slow);
|
|
2503
|
+
});
|
|
2504
|
+
let fast = false;
|
|
2505
|
+
ctl(t("ui.fast"), (b) => {
|
|
2506
|
+
fast = !fast;
|
|
2507
|
+
b.classList.toggle("on", fast);
|
|
2508
|
+
o.onFast(fast);
|
|
2509
|
+
});
|
|
2510
|
+
const tapBtn = ctl(t("ui.tap"), () => o.onTap());
|
|
2511
|
+
const qSel = mk("select");
|
|
2512
|
+
for (const q of ["low", "medium", "high"]) {
|
|
2513
|
+
const op = mk("option", void 0, t(`ui.quality.${q}`));
|
|
2514
|
+
op.value = q;
|
|
2515
|
+
qSel.appendChild(op);
|
|
2516
|
+
}
|
|
2517
|
+
qSel.value = o.quality;
|
|
2518
|
+
qSel.onchange = () => o.onQuality(qSel.value);
|
|
2519
|
+
const qLabel = mk("label", void 0, t("ui.quality") + " ");
|
|
2520
|
+
qLabel.appendChild(qSel);
|
|
2521
|
+
controls.appendChild(qLabel);
|
|
2522
|
+
const count = mk("span");
|
|
2523
|
+
controls.appendChild(count);
|
|
2524
|
+
cat.appendChild(controls);
|
|
2525
|
+
tests.appendChild(mk("h3", void 0, t("ui.tests")));
|
|
2526
|
+
const runAll = mk("button", "tr-btn", t("ui.run_all"));
|
|
2527
|
+
runAll.type = "button";
|
|
2528
|
+
const summary = mk("div", "tr-summary");
|
|
2529
|
+
const list = mk("div", "list");
|
|
2530
|
+
tests.append(runAll, summary, list);
|
|
2531
|
+
const scBtns = /* @__PURE__ */ new Map();
|
|
2532
|
+
let busy = false;
|
|
2533
|
+
const mark = (rep) => {
|
|
2534
|
+
if (!rep) return;
|
|
2535
|
+
for (const r of rep.results) {
|
|
2536
|
+
const b = scBtns.get(r.id);
|
|
2537
|
+
if (!b) continue;
|
|
2538
|
+
b.classList.remove("pass", "fail");
|
|
2539
|
+
b.classList.add(r.ok ? "pass" : "fail");
|
|
2540
|
+
b.textContent = `${r.ok ? "\u2713" : "\u2717"} ${r.name}`;
|
|
2541
|
+
}
|
|
2542
|
+
summary.textContent = t("ui.report", { passed: rep.passed, failed: rep.failed });
|
|
2543
|
+
};
|
|
2544
|
+
const renderScenarios = () => {
|
|
2545
|
+
list.textContent = "";
|
|
2546
|
+
scBtns.clear();
|
|
2547
|
+
for (const s of o.scenarios()) {
|
|
2548
|
+
const b = mk("button", "tr-btn", s.name);
|
|
2549
|
+
b.type = "button";
|
|
2550
|
+
b.onclick = async () => {
|
|
2551
|
+
if (busy) return;
|
|
2552
|
+
busy = true;
|
|
2553
|
+
try {
|
|
2554
|
+
mark(await o.onRun(s));
|
|
2555
|
+
} finally {
|
|
2556
|
+
busy = false;
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
scBtns.set(s.id, b);
|
|
2560
|
+
list.appendChild(b);
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
renderScenarios();
|
|
2564
|
+
runAll.onclick = async () => {
|
|
2565
|
+
if (busy) return;
|
|
2566
|
+
busy = true;
|
|
2567
|
+
summary.textContent = t("ui.running");
|
|
2568
|
+
try {
|
|
2569
|
+
mark(await o.onRunAll());
|
|
2570
|
+
} finally {
|
|
2571
|
+
busy = false;
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const toggle = mk("button", "tr-btn tr-toggle", t("ui.panel"));
|
|
2575
|
+
toggle.type = "button";
|
|
2576
|
+
const togglePanels = () => {
|
|
2577
|
+
root.classList.toggle("open");
|
|
2578
|
+
o.onResize();
|
|
2579
|
+
};
|
|
2580
|
+
toggle.onclick = togglePanels;
|
|
2581
|
+
root.append(cross, top, bottom, help, left, right, toggle);
|
|
2582
|
+
const bar = (value, max, cls) => {
|
|
2583
|
+
const b = mk("div", `tr-bar ${cls}`);
|
|
2584
|
+
const i = mk("i");
|
|
2585
|
+
i.style.width = `${Math.max(0, Math.min(100, value / Math.max(1e-9, max) * 100))}%`;
|
|
2586
|
+
b.appendChild(i);
|
|
2587
|
+
return b;
|
|
2588
|
+
};
|
|
2589
|
+
const row = (k, v) => {
|
|
2590
|
+
const r = mk("div", "r");
|
|
2591
|
+
r.append(mk("span", void 0, k), mk("span", void 0, v));
|
|
2592
|
+
return r;
|
|
2593
|
+
};
|
|
2594
|
+
const num = (n, d = 1) => {
|
|
2595
|
+
const s = (Math.round(n * 10 ** d) / 10 ** d).toFixed(d);
|
|
2596
|
+
return t("locale.decimal") === "," ? s.replace(".", ",") : s;
|
|
2597
|
+
};
|
|
2598
|
+
const renderInspector = (i) => {
|
|
2599
|
+
insp.textContent = "";
|
|
2600
|
+
insp.appendChild(mk("h3", void 0, t("ui.inspector")));
|
|
2601
|
+
if (!i) {
|
|
2602
|
+
insp.appendChild(mk("div", void 0, t("ui.aim_at_something")));
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
insp.appendChild(mk("div", "t", `${i.object} \u203A ${i.part}`));
|
|
2606
|
+
insp.appendChild(row(t("ins.material"), i.material));
|
|
2607
|
+
insp.appendChild(row(t("ins.status"), i.status + (i.burning ? ` \xB7 ${t("ins.burning")}` : "")));
|
|
2608
|
+
insp.appendChild(row(t("ins.hp"), `${num(i.hp, 0)} / ${num(i.maxHp, 0)}`));
|
|
2609
|
+
const frac = i.hp / Math.max(1e-9, i.maxHp);
|
|
2610
|
+
insp.appendChild(bar(i.hp, i.maxHp, frac < 0.25 ? "bad" : frac < 0.6 ? "warn" : "hp"));
|
|
2611
|
+
insp.appendChild(row(t("ins.temperature"), `${num(i.temperature, 0)} \xB0C`));
|
|
2612
|
+
if (i.char > 0.01) insp.appendChild(row(t("ins.char"), `${num(i.char * 100, 0)} %`));
|
|
2613
|
+
if (i.melt > 0.01) insp.appendChild(row(t("ins.melt"), `${num(i.melt * 100, 0)} %`));
|
|
2614
|
+
if (i.liters !== void 0) insp.appendChild(row(t("ins.liters"), `${num(i.liters, 2)} l`));
|
|
2615
|
+
const cn = (what, n) => t(`count.${what}.${n === 1 ? "one" : "other"}`, { n });
|
|
2616
|
+
insp.appendChild(row(t("ins.damage"), t("ins.damage_counts", { dents: cn("dent", i.dents), holes: cn("hole", i.holes), cracks: cn("crack", i.cracks) })));
|
|
2617
|
+
insp.appendChild(row(t("ins.mass"), `${num(i.mass, 1)} kg`));
|
|
2618
|
+
if (i.load !== void 0) insp.appendChild(row(t("ins.load"), `${num(i.load, 0)} kg`));
|
|
2619
|
+
if (i.stress !== void 0) {
|
|
2620
|
+
insp.appendChild(row(t("ins.stress"), `${num(i.stress * 100, 0)} %`));
|
|
2621
|
+
insp.appendChild(bar(Math.min(i.stress, 1.2), 1.2, i.stress > 1 ? "bad" : i.stress > 0.7 ? "warn" : "hp"));
|
|
2622
|
+
}
|
|
2623
|
+
if (i.joints.length) {
|
|
2624
|
+
insp.appendChild(mk("div", void 0, t("ins.joints") + ":"));
|
|
2625
|
+
for (const j of i.joints) {
|
|
2626
|
+
const tools = j.tools.length ? j.tools.join(", ") : t("tool.none");
|
|
2627
|
+
insp.appendChild(mk("div", void 0, `\u2022 ${j.kind} (${j.id})${j.open ? ` \xB7 ${t("ins.open")}` : ` \u2192 ${tools}`}`));
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
if (i.note) insp.appendChild(mk("div", void 0, i.note));
|
|
2631
|
+
};
|
|
2632
|
+
let lastAnim = "";
|
|
2633
|
+
const renderAnimations = (a) => {
|
|
2634
|
+
anim.textContent = "";
|
|
2635
|
+
anim.appendChild(mk("h3", void 0, t("ui.animations")));
|
|
2636
|
+
if (!a) {
|
|
2637
|
+
anim.appendChild(mk("div", "sub", t("ui.animations_aim")));
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
anim.appendChild(mk("div", "sub", a.name));
|
|
2641
|
+
if (a.clips.length === 0) {
|
|
2642
|
+
anim.appendChild(mk("div", "sub", t("ui.animations_none")));
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const l = mk("div", "list");
|
|
2646
|
+
for (const c of a.clips) {
|
|
2647
|
+
const b = mk("button", a.playing === c.id ? "tr-btn on" : "tr-btn", `\u25B6 ${t(`anim.${c.id}`) === `anim.${c.id}` ? c.id : t(`anim.${c.id}`)} (${num(c.duration, 1)} s)`);
|
|
2648
|
+
b.type = "button";
|
|
2649
|
+
b.onclick = () => o.onPlay(a.objectId, c.id);
|
|
2650
|
+
l.appendChild(b);
|
|
2651
|
+
}
|
|
2652
|
+
anim.appendChild(l);
|
|
2653
|
+
};
|
|
2654
|
+
renderAnimations(null);
|
|
2655
|
+
let toastTimer = null;
|
|
2656
|
+
const slotsEl = [];
|
|
2657
|
+
let lastSlotsKey = "";
|
|
2658
|
+
let lastInspect = "";
|
|
2659
|
+
let hudPage = 0;
|
|
2660
|
+
const api = {
|
|
2661
|
+
hud(s) {
|
|
2662
|
+
toolBox.textContent = "";
|
|
2663
|
+
toolBox.append(document.createTextNode(s.toolName));
|
|
2664
|
+
if (s.ammo) toolBox.append(mk("small", void 0, s.ammo));
|
|
2665
|
+
if (s.status) status.textContent = s.status;
|
|
2666
|
+
else if (!toastTimer) status.textContent = "";
|
|
2667
|
+
if (s.progress) {
|
|
2668
|
+
prog.style.display = "block";
|
|
2669
|
+
progLabel.textContent = s.progress.label;
|
|
2670
|
+
progFill.style.width = `${Math.round(s.progress.value * 100)}%`;
|
|
2671
|
+
} else prog.style.display = "none";
|
|
2672
|
+
if (s.held) {
|
|
2673
|
+
held.style.display = "block";
|
|
2674
|
+
held.textContent = "";
|
|
2675
|
+
held.append(document.createTextNode(`${s.held.name} \xB7 `));
|
|
2676
|
+
const keys = [
|
|
2677
|
+
["E", t("hud.drop")],
|
|
2678
|
+
["R", t("hud.rotate")],
|
|
2679
|
+
["G", t("hud.throw")],
|
|
2680
|
+
["Z", t("hud.tilt")],
|
|
2681
|
+
...s.held.uses.map((u) => [u.key, u.label])
|
|
2682
|
+
];
|
|
2683
|
+
if (s.held.weapon) keys.push(["H", t("hud.equip", { weapon: s.held.weapon })]);
|
|
2684
|
+
for (const [k, label] of keys) {
|
|
2685
|
+
held.append(mk("kbd", void 0, k), document.createTextNode(` ${label} `));
|
|
2686
|
+
}
|
|
2687
|
+
} else held.style.display = "none";
|
|
2688
|
+
const key = s.slots.join("|") + s.page;
|
|
2689
|
+
if (key !== lastSlotsKey) {
|
|
2690
|
+
lastSlotsKey = key;
|
|
2691
|
+
hotbar.textContent = "";
|
|
2692
|
+
slotsEl.length = 0;
|
|
2693
|
+
hudPage = s.page - 1;
|
|
2694
|
+
s.slots.forEach((name, i) => {
|
|
2695
|
+
const b = mk("button", "tr-slot");
|
|
2696
|
+
b.type = "button";
|
|
2697
|
+
b.append(mk("b", void 0, String((i + 1) % 10)), document.createTextNode(name));
|
|
2698
|
+
b.title = name;
|
|
2699
|
+
b.onclick = () => o.onTool(hudPage * 10 + i);
|
|
2700
|
+
hotbar.appendChild(b);
|
|
2701
|
+
slotsEl.push(b);
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
|
|
2705
|
+
tapBtn.classList.toggle("on", s.tap);
|
|
2706
|
+
count.textContent = t("ui.objects", { n: s.objects });
|
|
2707
|
+
const ak = JSON.stringify(s.animations);
|
|
2708
|
+
if (ak !== lastAnim) {
|
|
2709
|
+
lastAnim = ak;
|
|
2710
|
+
renderAnimations(s.animations);
|
|
2711
|
+
}
|
|
2712
|
+
const ik = JSON.stringify(s.inspect);
|
|
2713
|
+
if (ik !== lastInspect) {
|
|
2714
|
+
lastInspect = ik;
|
|
2715
|
+
renderInspector(s.inspect);
|
|
2716
|
+
}
|
|
2717
|
+
},
|
|
2718
|
+
toast(text) {
|
|
2719
|
+
status.textContent = text;
|
|
2720
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
2721
|
+
toastTimer = setTimeout(() => {
|
|
2722
|
+
toastTimer = null;
|
|
2723
|
+
}, 2e3);
|
|
2724
|
+
},
|
|
2725
|
+
refresh() {
|
|
2726
|
+
renderTabs();
|
|
2727
|
+
renderScenarios();
|
|
2728
|
+
},
|
|
2729
|
+
togglePanels,
|
|
2730
|
+
dispose() {
|
|
2731
|
+
offLog();
|
|
2732
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
2733
|
+
root.remove();
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
rebuildLog();
|
|
2737
|
+
return api;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// src/room.ts
|
|
2741
|
+
var FIXED_DT = 1 / 60;
|
|
2742
|
+
var v3 = (v) => [v.x, v.y, v.z];
|
|
2743
|
+
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
2744
|
+
function createTestRoom(options) {
|
|
2745
|
+
const THREE = options.THREE;
|
|
2746
|
+
const locale = options.locale ?? "en";
|
|
2747
|
+
const S = resolveStrings(locale);
|
|
2748
|
+
const dStrings = resolveDestructionStrings(locale);
|
|
2749
|
+
const fStrings = resolveFluidStrings(locale);
|
|
2750
|
+
const t = (key, vars) => {
|
|
2751
|
+
let s = S.text[key] ?? key;
|
|
2752
|
+
if (vars) for (const k of Object.keys(vars)) s = s.split(`{${k}}`).join(String(vars[k]));
|
|
2753
|
+
return s;
|
|
2754
|
+
};
|
|
2755
|
+
const num = (n, digits = 1) => {
|
|
2756
|
+
const s = (Math.round(n * 10 ** digits) / 10 ** digits).toFixed(digits);
|
|
2757
|
+
return locale.toLowerCase().startsWith("pt") ? s.replace(".", ",") : s;
|
|
2758
|
+
};
|
|
2759
|
+
const ownRenderer = !options.renderer;
|
|
2760
|
+
const gl = options.renderer ?? new THREE.WebGLRenderer({ antialias: true });
|
|
2761
|
+
const container = options.container;
|
|
2762
|
+
const width = () => container.clientWidth || 960;
|
|
2763
|
+
const height = () => container.clientHeight || 600;
|
|
2764
|
+
if (ownRenderer) {
|
|
2765
|
+
const r = gl;
|
|
2766
|
+
r.setPixelRatio(Math.min(typeof devicePixelRatio === "number" ? devicePixelRatio : 1, 2));
|
|
2767
|
+
r.shadowMap.enabled = true;
|
|
2768
|
+
r.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
2769
|
+
r.toneMapping = THREE.ACESFilmicToneMapping;
|
|
2770
|
+
r.toneMappingExposure = 1;
|
|
2771
|
+
r.outputColorSpace = THREE.SRGBColorSpace;
|
|
2772
|
+
container.appendChild(r.domElement);
|
|
2773
|
+
}
|
|
2774
|
+
gl.setSize(width(), height());
|
|
2775
|
+
const dom = gl.domElement;
|
|
2776
|
+
if (dom && !dom.hasAttribute?.("tabindex")) dom.setAttribute?.("tabindex", "0");
|
|
2777
|
+
const scene = options.scene ?? new THREE.Scene();
|
|
2778
|
+
const camera = options.camera ?? new THREE.PerspectiveCamera(72, width() / height(), 0.05, 140);
|
|
2779
|
+
if (!options.camera) scene.add(camera);
|
|
2780
|
+
const resize = () => {
|
|
2781
|
+
const w = width();
|
|
2782
|
+
const h = height();
|
|
2783
|
+
gl.setSize(w, h);
|
|
2784
|
+
camera.aspect = w / h;
|
|
2785
|
+
camera.updateProjectionMatrix();
|
|
2786
|
+
};
|
|
2787
|
+
const onResize = () => resize();
|
|
2788
|
+
window.addEventListener("resize", onResize);
|
|
2789
|
+
const arenaSize = options.arenaSize ?? 14;
|
|
2790
|
+
const arena = createArena(THREE, arenaSize, scene);
|
|
2791
|
+
scene.add(arena.group);
|
|
2792
|
+
const ownWorld = !options.world;
|
|
2793
|
+
const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...DEMO_MATERIALS, ...options.worldOptions?.materials ?? []] });
|
|
2794
|
+
const pristineWorld = ownWorld ? world.serialize() : null;
|
|
2795
|
+
const tableOk = () => {
|
|
2796
|
+
const p = world.part(arena.table.id, arena.table.topPart);
|
|
2797
|
+
return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes";
|
|
2798
|
+
};
|
|
2799
|
+
const groundAt = (x, z) => x > arena.table.minX && x < arena.table.maxX && z > arena.table.minZ && z < arena.table.maxZ && tableOk() ? arena.table.top : 0;
|
|
2800
|
+
const ownFluids = !options.fluids;
|
|
2801
|
+
const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
|
|
2802
|
+
const pristineFluids = ownFluids ? fluids.serialize() : null;
|
|
2803
|
+
for (const s of [...arena.surfaces, ...options.surfaces ?? []]) fluids.addSurface(s);
|
|
2804
|
+
const itemUseDefs = [...ITEM_USES, ...options.itemUses ?? []];
|
|
2805
|
+
const quality = options.quality ?? "medium";
|
|
2806
|
+
const pointScale = height() / (2 * Math.tan(camera.fov * Math.PI / 360));
|
|
2807
|
+
const rendererPieces = /* @__PURE__ */ new Map();
|
|
2808
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2809
|
+
const splitOf = /* @__PURE__ */ new Map();
|
|
2810
|
+
let simTime = 0;
|
|
2811
|
+
const log = createLog();
|
|
2812
|
+
log.setClock(() => simTime);
|
|
2813
|
+
const renderer = createDestructionRenderer(THREE, {
|
|
2814
|
+
scene,
|
|
2815
|
+
world,
|
|
2816
|
+
quality,
|
|
2817
|
+
groundAt,
|
|
2818
|
+
pointScale,
|
|
2819
|
+
onSound: options.onSound,
|
|
2820
|
+
onPiece: (piece) => {
|
|
2821
|
+
rendererPieces.set(piece.objectId, piece);
|
|
2822
|
+
loose.push(piece);
|
|
2823
|
+
const st = world.state(piece.objectId);
|
|
2824
|
+
if (!st) return;
|
|
2825
|
+
let best = null;
|
|
2826
|
+
let bestD = Infinity;
|
|
2827
|
+
for (const e of entries.values()) {
|
|
2828
|
+
const spec = specs.get(e.so.objectId);
|
|
2829
|
+
if (!spec) continue;
|
|
2830
|
+
const ids = new Set(spec.parts.map((p) => p.id));
|
|
2831
|
+
if (!st.parts.every((p) => ids.has(p.id))) continue;
|
|
2832
|
+
const d = e.so.root.position.distanceTo(piece.root.position);
|
|
2833
|
+
if (d < bestD) {
|
|
2834
|
+
bestD = d;
|
|
2835
|
+
best = e;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
if (best) splitOf.set(piece.objectId, best);
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale });
|
|
2842
|
+
const specs = /* @__PURE__ */ new Map();
|
|
2843
|
+
const loose = [];
|
|
2844
|
+
const arsenal = options.arsenal ?? createArsenal({ locale, weapons: options.weapons, seed: 1 });
|
|
2845
|
+
const loadout = (options.loadout ?? DEFAULT_LOADOUT).filter((id) => arsenal.weapons.has(id));
|
|
2846
|
+
const tools = /* @__PURE__ */ new Map();
|
|
2847
|
+
let toolIndex = Math.max(0, loadout.indexOf("hand"));
|
|
2848
|
+
let toolId = loadout[toolIndex] ?? "hand";
|
|
2849
|
+
const toolState = (id) => {
|
|
2850
|
+
let s = tools.get(id);
|
|
2851
|
+
if (!s) {
|
|
2852
|
+
s = arsenal.create(id, { seed: 7, reserve: 999 });
|
|
2853
|
+
s.refill();
|
|
2854
|
+
tools.set(id, s);
|
|
2855
|
+
}
|
|
2856
|
+
return s;
|
|
2857
|
+
};
|
|
2858
|
+
let tool = toolState(toolId);
|
|
2859
|
+
const catalogMap = /* @__PURE__ */ new Map();
|
|
2860
|
+
for (const i of demoItems(locale)) catalogMap.set(i.id, i);
|
|
2861
|
+
for (const i of options.catalog) catalogMap.set(i.id, i);
|
|
2862
|
+
const catalog = [...options.catalog];
|
|
2863
|
+
const entryOf = (objectId) => entries.get(objectId) ?? splitOf.get(objectId);
|
|
2864
|
+
const nameOf = (objectId) => {
|
|
2865
|
+
const e = entries.get(objectId);
|
|
2866
|
+
if (e) return e.name;
|
|
2867
|
+
const s = splitOf.get(objectId);
|
|
2868
|
+
return s ? `${s.name} (${t("piece")})` : objectId;
|
|
2869
|
+
};
|
|
2870
|
+
const narrator = createNarrator({
|
|
2871
|
+
world,
|
|
2872
|
+
fluids,
|
|
2873
|
+
log,
|
|
2874
|
+
now: () => simTime,
|
|
2875
|
+
t,
|
|
2876
|
+
num,
|
|
2877
|
+
nameOf,
|
|
2878
|
+
itemIdOf: (id) => entryOf(id)?.so.itemId,
|
|
2879
|
+
materialName: (id) => S.text[`material.${id}`] ?? dStrings.materialNames[id] ?? id,
|
|
2880
|
+
liquidName: (id) => fStrings.liquids[id] ?? id,
|
|
2881
|
+
jointKind: (k) => dStrings.jointKinds[k] ?? k,
|
|
2882
|
+
toolName: (k) => dStrings.toolKinds[k] ?? k,
|
|
2883
|
+
locatePart: (objectId, partId) => {
|
|
2884
|
+
const own = world.part(objectId, partId);
|
|
2885
|
+
if (own) return { objectId, part: own };
|
|
2886
|
+
for (const [id, owner] of splitOf) {
|
|
2887
|
+
if (owner.so.objectId !== objectId) continue;
|
|
2888
|
+
const p = world.part(id, partId);
|
|
2889
|
+
if (p) return { objectId: id, part: p };
|
|
2890
|
+
}
|
|
2891
|
+
return null;
|
|
2892
|
+
},
|
|
2893
|
+
containerName: (cid) => {
|
|
2894
|
+
for (const e of entries.values()) if (e.containerId === cid) return { name: e.name, objectId: e.so.objectId, itemId: e.so.itemId };
|
|
2895
|
+
return void 0;
|
|
2896
|
+
}
|
|
2897
|
+
});
|
|
2898
|
+
let pending = [];
|
|
2899
|
+
const eventListeners = /* @__PURE__ */ new Set();
|
|
2900
|
+
const listeners = { spawn: /* @__PURE__ */ new Set(), tool: /* @__PURE__ */ new Set(), grab: /* @__PURE__ */ new Set(), throw: /* @__PURE__ */ new Set(), event: eventListeners };
|
|
2901
|
+
const fire = (type, data) => {
|
|
2902
|
+
for (const fn of listeners[type] ?? []) fn(data);
|
|
2903
|
+
};
|
|
2904
|
+
world.on("*", (ev) => {
|
|
2905
|
+
pending.push(ev);
|
|
2906
|
+
});
|
|
2907
|
+
fluids.on("*", (ev) => {
|
|
2908
|
+
narrator.fluid(ev);
|
|
2909
|
+
fire("event", ev);
|
|
2910
|
+
});
|
|
2911
|
+
const action = (type, text, extra = {}) => {
|
|
2912
|
+
log.add({ channel: "action", type, text, ...extra });
|
|
2913
|
+
};
|
|
2914
|
+
const tmpV = new THREE.Vector3();
|
|
2915
|
+
const tmpV2 = new THREE.Vector3();
|
|
2916
|
+
const tmpQ = new THREE.Quaternion();
|
|
2917
|
+
const tmpM = new THREE.Matrix4();
|
|
2918
|
+
const hostOf = (e) => {
|
|
2919
|
+
if (e.fluidPart && !e.arena && !world.part(e.so.objectId, e.fluidPart)) {
|
|
2920
|
+
for (const [id, owner] of splitOf) {
|
|
2921
|
+
if (owner === e && world.part(id, e.fluidPart)) {
|
|
2922
|
+
const p = rendererPieces.get(id);
|
|
2923
|
+
if (p) return { objectId: id, root: p.root };
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
return { objectId: e.so.objectId, root: e.so.root };
|
|
2928
|
+
};
|
|
2929
|
+
const puncture = (ev) => {
|
|
2930
|
+
const e = entryOf(ev.object);
|
|
2931
|
+
if (!e?.containerId || e.fluidPart !== ev.part) return;
|
|
2932
|
+
const host = hostOf(e);
|
|
2933
|
+
host.root.updateMatrixWorld(true);
|
|
2934
|
+
tmpM.copy(host.root.matrixWorld).invert();
|
|
2935
|
+
const local = tmpV.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(tmpM);
|
|
2936
|
+
local.x -= e.fluidOffset[0];
|
|
2937
|
+
local.y -= e.fluidOffset[1];
|
|
2938
|
+
local.z -= e.fluidOffset[2];
|
|
2939
|
+
host.root.getWorldQuaternion(tmpQ).invert();
|
|
2940
|
+
const n = tmpV2.set(ev.direction[0], ev.direction[1], ev.direction[2]).applyQuaternion(tmpQ);
|
|
2941
|
+
const part = world.part(host.objectId, ev.part);
|
|
2942
|
+
const hole = part?.holes[part.holes.length - 1];
|
|
2943
|
+
fluids.puncture(e.containerId, [local.x, local.y, local.z], [n.x, n.y, n.z], hole?.diameter ?? 9e-3);
|
|
2944
|
+
};
|
|
2945
|
+
const flush = () => {
|
|
2946
|
+
if (pending.length === 0) return;
|
|
2947
|
+
const batch = pending;
|
|
2948
|
+
pending = [];
|
|
2949
|
+
narrator.events(batch);
|
|
2950
|
+
for (const ev of batch) {
|
|
2951
|
+
if (ev.type === "leak") puncture(ev);
|
|
2952
|
+
else if (ev.type === "ignite") igniteNear(ev.point, 0.9);
|
|
2953
|
+
fire("event", ev);
|
|
2954
|
+
options.onEvent?.(ev);
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
log.setBeforeRead(flush);
|
|
2958
|
+
let useHeld = false;
|
|
2959
|
+
let useFirst = false;
|
|
2960
|
+
const player = createPlayer(THREE, {
|
|
2961
|
+
camera,
|
|
2962
|
+
dom,
|
|
2963
|
+
colliders: arena.colliders,
|
|
2964
|
+
limit: arenaSize,
|
|
2965
|
+
start: arena.playerStart,
|
|
2966
|
+
hooks: {
|
|
2967
|
+
primaryDown: () => {
|
|
2968
|
+
useHeld = true;
|
|
2969
|
+
useFirst = true;
|
|
2970
|
+
},
|
|
2971
|
+
primaryUp: () => {
|
|
2972
|
+
useHeld = false;
|
|
2973
|
+
tool.release();
|
|
2974
|
+
},
|
|
2975
|
+
secondary: () => {
|
|
2976
|
+
if (heldEntry) throwHeld();
|
|
2977
|
+
else grab();
|
|
2978
|
+
},
|
|
2979
|
+
key: (code, down, e) => onKey(code, down, e),
|
|
2980
|
+
wheel: (dir) => cycleTool(dir)
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2983
|
+
player.look(arena.playerStart, [0, 1.5, arena.wallZ]);
|
|
2984
|
+
let counter = 0;
|
|
2985
|
+
let slot = 0;
|
|
2986
|
+
const maxCarry = options.maxCarryKg ?? 60;
|
|
2987
|
+
const massOfObject = (id) => {
|
|
2988
|
+
const st = world.state(id);
|
|
2989
|
+
let m = 0;
|
|
2990
|
+
for (const p of st?.parts ?? []) if (p.status !== "broken" && p.status !== "melted" && p.status !== "ashes") m += p.mass;
|
|
2991
|
+
return m;
|
|
2992
|
+
};
|
|
2993
|
+
const registerEntry = (objectId, itemId, root, spec, name, parts, item, arenaObject, movable) => {
|
|
2994
|
+
specs.set(objectId, spec);
|
|
2995
|
+
renderer.bind(objectId, root, parts ? { parts } : void 0);
|
|
2996
|
+
const so = { objectId, itemId, root, movable };
|
|
2997
|
+
const e = { so, item, name, arena: arenaObject, fluidOffset: [0, 0, 0], uses: [], piece: null, last: [root.position.x, root.position.y, root.position.z], velocity: [0, 0, 0] };
|
|
2998
|
+
entries.set(objectId, e);
|
|
2999
|
+
if (movable) {
|
|
3000
|
+
e.piece = renderer.makePiece(objectId, { mass: Math.max(0.2, massOfObject(objectId)) });
|
|
3001
|
+
rendererPieces.set(objectId, e.piece);
|
|
3002
|
+
}
|
|
3003
|
+
const f = item?.fluid;
|
|
3004
|
+
if (f) {
|
|
3005
|
+
e.fluidPart = f.part;
|
|
3006
|
+
e.fluidOffset = f.offset ?? [0, 0, 0];
|
|
3007
|
+
e.uses = f.uses ?? ["drink", "sip", "pour"];
|
|
3008
|
+
root.updateMatrixWorld(true);
|
|
3009
|
+
const q = root.getWorldQuaternion(tmpQ);
|
|
3010
|
+
const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(root.matrixWorld);
|
|
3011
|
+
e.containerId = fluids.addContainer(f.container, { id: `${objectId}_fluid`, position: [p.x, p.y, p.z], rotation: [q.x, q.y, q.z, q.w] });
|
|
3012
|
+
fluidView.bindContainer(e.containerId, root, { innerOffset: e.fluidOffset });
|
|
3013
|
+
}
|
|
3014
|
+
return e;
|
|
3015
|
+
};
|
|
3016
|
+
const resolveSpec = (item, root) => {
|
|
3017
|
+
if (typeof item.spec === "function") return { spec: structuredCloneSpec(item.spec(root)) };
|
|
3018
|
+
if (item.spec) return { spec: structuredCloneSpec(item.spec) };
|
|
3019
|
+
const r = specFromObject3D(THREE, root, { name: item.id, ...item.specOptions });
|
|
3020
|
+
return { spec: r.spec, parts: r.parts };
|
|
3021
|
+
};
|
|
3022
|
+
const addArena = () => {
|
|
3023
|
+
for (const o of arena.objects) {
|
|
3024
|
+
const root = buildFromSpec(THREE, o.spec, { colors: o.colors });
|
|
3025
|
+
root.position.set(o.position[0], o.position[1], o.position[2]);
|
|
3026
|
+
scene.add(root);
|
|
3027
|
+
root.updateMatrixWorld(true);
|
|
3028
|
+
world.add(structuredCloneSpec(o.spec), { id: o.id, position: o.position });
|
|
3029
|
+
registerEntry(o.id, o.id, root, o.spec, S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
addArena();
|
|
3033
|
+
const spawn = (itemId, position, rotationY) => {
|
|
3034
|
+
const item = catalogMap.get(itemId);
|
|
3035
|
+
if (!item) throw new Error(`game-test-room: unknown item "${itemId}"`);
|
|
3036
|
+
const root = item.build(THREE);
|
|
3037
|
+
let pos;
|
|
3038
|
+
if (position) pos = [position[0], position[1], position[2]];
|
|
3039
|
+
else {
|
|
3040
|
+
const fx = -Math.sin(player.yaw);
|
|
3041
|
+
const fz = -Math.cos(player.yaw);
|
|
3042
|
+
const rx = Math.cos(player.yaw);
|
|
3043
|
+
const rz = -Math.sin(player.yaw);
|
|
3044
|
+
const side = (slot++ % 5 - 2) * 0.55;
|
|
3045
|
+
const x = camera.position.x + fx * 2.4 + rx * side;
|
|
3046
|
+
const z = camera.position.z + fz * 2.4 + rz * side;
|
|
3047
|
+
const lim = arenaSize - 1;
|
|
3048
|
+
pos = [Math.max(-lim, Math.min(lim, x)), groundAt(x, z), Math.max(-lim, Math.min(lim, z))];
|
|
3049
|
+
}
|
|
3050
|
+
root.position.set(pos[0], pos[1], pos[2]);
|
|
3051
|
+
root.rotation.y = rotationY ?? player.yaw;
|
|
3052
|
+
root.name = item.id;
|
|
3053
|
+
scene.add(root);
|
|
3054
|
+
root.updateMatrixWorld(true);
|
|
3055
|
+
const objectId = `${item.id}_${++counter}`;
|
|
3056
|
+
let own;
|
|
3057
|
+
let resolved;
|
|
3058
|
+
if (item.impl) {
|
|
3059
|
+
own = item.impl(root, objectId);
|
|
3060
|
+
if (own.id !== objectId) throw new Error(`game-test-room: item "${item.id}": DestructibleImpl.id is "${own.id}", the room gave it "${objectId}" (use the id passed to impl)`);
|
|
3061
|
+
resolved = { spec: structuredCloneSpec(own.describe()) };
|
|
3062
|
+
} else resolved = resolveSpec(item, root);
|
|
3063
|
+
const { spec, parts } = resolved;
|
|
3064
|
+
root.updateMatrixWorld(true);
|
|
3065
|
+
const mass = spec.parts.reduce((m, p) => m + (p.mass ?? 0), 0);
|
|
3066
|
+
const heavy = (mass || massGuess(spec)) > maxCarry;
|
|
3067
|
+
const movable = own ? false : item.movable ?? !heavy;
|
|
3068
|
+
if (movable) {
|
|
3069
|
+
const box2 = new THREE.Box3().setFromObject(root);
|
|
3070
|
+
const base = Math.max(pos[1], groundAt(pos[0], pos[2]));
|
|
3071
|
+
pos = [pos[0], pos[1] + (base - box2.min.y), pos[2]];
|
|
3072
|
+
root.position.set(pos[0], pos[1], pos[2]);
|
|
3073
|
+
root.updateMatrixWorld(true);
|
|
3074
|
+
}
|
|
3075
|
+
const q = root.getWorldQuaternion(tmpQ);
|
|
3076
|
+
if (own) world.register(own, { id: objectId, position: pos, rotation: [q.x, q.y, q.z, q.w] });
|
|
3077
|
+
else world.add(spec, { id: objectId, position: pos, rotation: [q.x, q.y, q.z, q.w] });
|
|
3078
|
+
const e = registerEntry(objectId, item.id, root, spec, item.name, parts, item, false, movable);
|
|
3079
|
+
action("spawn", t("act.spawn", { name: item.name, x: num(pos[0], 1), z: num(pos[2], 1) }), { objectId, itemId: item.id, data: { position: pos, movable, massKg: Math.round(massOfObject(objectId) * 10) / 10 } });
|
|
3080
|
+
fire("spawn", e.so);
|
|
3081
|
+
return e.so;
|
|
3082
|
+
};
|
|
3083
|
+
const massGuess = (spec) => {
|
|
3084
|
+
let m = 0;
|
|
3085
|
+
for (const p of spec.parts) {
|
|
3086
|
+
const s = p.shape;
|
|
3087
|
+
const vol = s.kind === "box" ? s.size[0] * s.size[1] * s.size[2] : s.kind === "cylinder" ? Math.PI * s.radius * s.radius * s.height : 4 / 3 * Math.PI * s.radius ** 3;
|
|
3088
|
+
m += p.mass ?? vol * (world.materials.get(p.material)?.density ?? 500) * (p.fill ?? 1);
|
|
3089
|
+
}
|
|
3090
|
+
return m;
|
|
3091
|
+
};
|
|
3092
|
+
const removeEntry = (id) => {
|
|
3093
|
+
const e = entries.get(id);
|
|
3094
|
+
if (!e) return;
|
|
3095
|
+
if (heldEntry === e) releaseHeld();
|
|
3096
|
+
for (let i = playing.length - 1; i >= 0; i--) if (playing[i].entry === e) playing.splice(i, 1);
|
|
3097
|
+
if (e.containerId) {
|
|
3098
|
+
fluidView.unbindContainer(e.containerId);
|
|
3099
|
+
fluids.removeContainer(e.containerId);
|
|
3100
|
+
}
|
|
3101
|
+
try {
|
|
3102
|
+
renderer.unbind(id);
|
|
3103
|
+
} catch {
|
|
3104
|
+
}
|
|
3105
|
+
if (world.has(id)) world.remove(id);
|
|
3106
|
+
e.so.root.removeFromParent();
|
|
3107
|
+
entries.delete(id);
|
|
3108
|
+
rendererPieces.delete(id);
|
|
3109
|
+
specs.delete(id);
|
|
3110
|
+
};
|
|
3111
|
+
const clearLoose = () => {
|
|
3112
|
+
for (const [id, owner] of [...splitOf]) {
|
|
3113
|
+
void owner;
|
|
3114
|
+
const p = rendererPieces.get(id);
|
|
3115
|
+
try {
|
|
3116
|
+
renderer.unbind(id);
|
|
3117
|
+
} catch {
|
|
3118
|
+
}
|
|
3119
|
+
if (world.has(id)) world.remove(id);
|
|
3120
|
+
p?.root.removeFromParent();
|
|
3121
|
+
rendererPieces.delete(id);
|
|
3122
|
+
splitOf.delete(id);
|
|
3123
|
+
}
|
|
3124
|
+
loose.length = 0;
|
|
3125
|
+
};
|
|
3126
|
+
const clearEverything = () => {
|
|
3127
|
+
if (heldEntry || heldPiece) releaseHeld();
|
|
3128
|
+
useItemPlayer = null;
|
|
3129
|
+
playing.length = 0;
|
|
3130
|
+
projectiles.length = 0;
|
|
3131
|
+
clearProjectileMeshes();
|
|
3132
|
+
fireZones.length = 0;
|
|
3133
|
+
burningStains.length = 0;
|
|
3134
|
+
for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
|
|
3135
|
+
clearLoose();
|
|
3136
|
+
for (const e of entries.values()) {
|
|
3137
|
+
world.reset(e.so.objectId);
|
|
3138
|
+
renderer.restore(e.so.objectId);
|
|
3139
|
+
}
|
|
3140
|
+
renderer.clear();
|
|
3141
|
+
fluids.clearStains([0, 0, 0], 1e4);
|
|
3142
|
+
fluidView.clear();
|
|
3143
|
+
fluids.setTap(TAP_ID, false);
|
|
3144
|
+
};
|
|
3145
|
+
const clear = () => {
|
|
3146
|
+
clearEverything();
|
|
3147
|
+
pending = [];
|
|
3148
|
+
action("clear", t("act.clear"));
|
|
3149
|
+
};
|
|
3150
|
+
const resetAll = () => {
|
|
3151
|
+
for (const e of entries.values()) {
|
|
3152
|
+
if (heldEntry === e) continue;
|
|
3153
|
+
world.reset(e.so.objectId);
|
|
3154
|
+
renderer.restore(e.so.objectId);
|
|
3155
|
+
if (e.containerId) {
|
|
3156
|
+
const c = fluids.container(e.containerId);
|
|
3157
|
+
const f = e.item?.fluid?.container;
|
|
3158
|
+
if (c && f && f.liquid) fluids.fill(e.containerId, f.liquid, Math.max(0, (f.liters ?? 0) - c.liters), f.temperature);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
action("reset", t("act.reset"));
|
|
3162
|
+
};
|
|
3163
|
+
const hardReset = () => {
|
|
3164
|
+
clearEverything();
|
|
3165
|
+
for (const id of [...entries.keys()]) removeEntry(id);
|
|
3166
|
+
tools.clear();
|
|
3167
|
+
tool = toolState(toolId);
|
|
3168
|
+
if (pristineWorld) {
|
|
3169
|
+
for (const id of world.ids()) world.remove(id);
|
|
3170
|
+
world.load(pristineWorld);
|
|
3171
|
+
}
|
|
3172
|
+
if (pristineFluids) {
|
|
3173
|
+
fluids.load(pristineFluids);
|
|
3174
|
+
for (const s of [...arena.surfaces, ...options.surfaces ?? []]) fluids.addSurface(s);
|
|
3175
|
+
}
|
|
3176
|
+
pending = [];
|
|
3177
|
+
rng = 12345;
|
|
3178
|
+
counter = 0;
|
|
3179
|
+
slot = 0;
|
|
3180
|
+
prevVy.clear();
|
|
3181
|
+
lastImpact.clear();
|
|
3182
|
+
addArena();
|
|
3183
|
+
timeScale = 1;
|
|
3184
|
+
fastForward = false;
|
|
3185
|
+
};
|
|
3186
|
+
const aimO = [0, 0, 0];
|
|
3187
|
+
const aimD = [0, 0, -1];
|
|
3188
|
+
const aim = () => {
|
|
3189
|
+
camera.updateMatrixWorld(true);
|
|
3190
|
+
camera.getWorldPosition(tmpV);
|
|
3191
|
+
camera.getWorldDirection(tmpV2);
|
|
3192
|
+
aimO[0] = tmpV.x;
|
|
3193
|
+
aimO[1] = tmpV.y;
|
|
3194
|
+
aimO[2] = tmpV.z;
|
|
3195
|
+
aimD[0] = tmpV2.x;
|
|
3196
|
+
aimD[1] = tmpV2.y;
|
|
3197
|
+
aimD[2] = tmpV2.z;
|
|
3198
|
+
};
|
|
3199
|
+
const FAR = [0, 0, 0, 1];
|
|
3200
|
+
const withoutHeld = (fn) => {
|
|
3201
|
+
const e = heldEntry;
|
|
3202
|
+
if (!e || !world.has(e.so.objectId)) return fn();
|
|
3203
|
+
const p = e.so.root.position;
|
|
3204
|
+
const q = e.so.root.quaternion;
|
|
3205
|
+
world.setTransform(e.so.objectId, [0, -5e3, 0], FAR);
|
|
3206
|
+
try {
|
|
3207
|
+
return fn();
|
|
3208
|
+
} finally {
|
|
3209
|
+
world.setTransform(e.so.objectId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w]);
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
const rayWorld = (o, d, max) => withoutHeld(() => world.raycast(o, d, max));
|
|
3213
|
+
let rng = 12345;
|
|
3214
|
+
const rnd = () => {
|
|
3215
|
+
rng = rng * 1664525 + 1013904223 >>> 0;
|
|
3216
|
+
return rng / 4294967296;
|
|
3217
|
+
};
|
|
3218
|
+
const muzzle = () => {
|
|
3219
|
+
aim();
|
|
3220
|
+
const e = camera.matrixWorld.elements;
|
|
3221
|
+
return [
|
|
3222
|
+
aimO[0] + aimD[0] * 0.45 + e[0] * 0.16 - e[4] * 0.1,
|
|
3223
|
+
aimO[1] + aimD[1] * 0.45 + e[1] * 0.16 - e[5] * 0.1,
|
|
3224
|
+
aimO[2] + aimD[2] * 0.45 + e[2] * 0.16 - e[6] * 0.1
|
|
3225
|
+
];
|
|
3226
|
+
};
|
|
3227
|
+
const tracers = [];
|
|
3228
|
+
const tracerMat = new THREE.LineBasicMaterial({ color: 16771496, transparent: true, opacity: 0.9 });
|
|
3229
|
+
const tracer = (a, b) => {
|
|
3230
|
+
let tr = tracers.find((x) => x.life <= 0);
|
|
3231
|
+
if (!tr) {
|
|
3232
|
+
if (tracers.length >= 12) return;
|
|
3233
|
+
const g = new THREE.BufferGeometry();
|
|
3234
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(6, 3));
|
|
3235
|
+
const line = new THREE.Line(g, tracerMat);
|
|
3236
|
+
line.frustumCulled = false;
|
|
3237
|
+
scene.add(line);
|
|
3238
|
+
tr = { line, life: 0 };
|
|
3239
|
+
tracers.push(tr);
|
|
3240
|
+
}
|
|
3241
|
+
const attr = tr.line.geometry.getAttribute("position");
|
|
3242
|
+
attr.setXYZ(0, a[0], a[1], a[2]);
|
|
3243
|
+
attr.setXYZ(1, b[0], b[1], b[2]);
|
|
3244
|
+
attr.needsUpdate = true;
|
|
3245
|
+
tr.line.visible = true;
|
|
3246
|
+
tr.life = 0.07;
|
|
3247
|
+
};
|
|
3248
|
+
const fireZones = [];
|
|
3249
|
+
const burningStains = [];
|
|
3250
|
+
const stainRadius = (s) => {
|
|
3251
|
+
let sum = 0;
|
|
3252
|
+
for (const r of s.front) sum += r;
|
|
3253
|
+
return s.front.length ? sum / s.front.length : 0.1;
|
|
3254
|
+
};
|
|
3255
|
+
const stainBurning = (s) => burningStains.some((b) => dist3(b.center, s.center) < 0.05);
|
|
3256
|
+
function igniteNear(point, reach) {
|
|
3257
|
+
let lit = false;
|
|
3258
|
+
let water = false;
|
|
3259
|
+
for (const s of fluids.stains()) {
|
|
3260
|
+
if (s.wetness < 0.05) continue;
|
|
3261
|
+
if (Math.hypot(s.center[0] - point[0], s.center[2] - point[2]) > reach + stainRadius(s) || Math.abs(s.center[1] - point[1]) > 1.5) continue;
|
|
3262
|
+
if (fluids.liquids.get(s.liquid)?.flammable) {
|
|
3263
|
+
if (stainBurning(s)) continue;
|
|
3264
|
+
const seconds = Math.max(3, Math.min(20, 4 + s.liters * 30 + stainRadius(s) * 6));
|
|
3265
|
+
burningStains.push({ center: [s.center[0], s.center[1], s.center[2]], remaining: seconds, acc: 0 });
|
|
3266
|
+
lit = true;
|
|
3267
|
+
log.add({
|
|
3268
|
+
channel: "event",
|
|
3269
|
+
type: "stain_ignite",
|
|
3270
|
+
text: t("ev.stain_ignite", { liquid: fStrings.liquids[s.liquid] ?? s.liquid, x: num(s.center[0], 1), z: num(s.center[2], 1), seconds: num(seconds, 0) }),
|
|
3271
|
+
data: { stain: s.id, liquid: s.liquid, seconds }
|
|
3272
|
+
});
|
|
3273
|
+
} else water = true;
|
|
3274
|
+
}
|
|
3275
|
+
if (water && !lit) {
|
|
3276
|
+
fluids.clearStains(point, Math.max(0.5, reach));
|
|
3277
|
+
log.coalesce("dry", { channel: "event", type: "stain_dried_by_fire", text: t("ev.fire_dries"), data: { point } }, 2);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
const hitFireAround = (point, radius, temperature, duration) => {
|
|
3281
|
+
for (const id of world.ids()) world.hit(id, { type: "fire", point, temperature, radius, duration, source: "fire" });
|
|
3282
|
+
};
|
|
3283
|
+
const updateFires = (dt) => {
|
|
3284
|
+
for (let i = fireZones.length - 1; i >= 0; i--) {
|
|
3285
|
+
const z = fireZones[i];
|
|
3286
|
+
z.remaining -= dt;
|
|
3287
|
+
z.acc += dt;
|
|
3288
|
+
if (z.acc >= 0.1) {
|
|
3289
|
+
hitFireAround(z.point, z.radius, z.temperature, z.acc);
|
|
3290
|
+
igniteNear(z.point, z.radius);
|
|
3291
|
+
z.acc = 0;
|
|
3292
|
+
}
|
|
3293
|
+
const a = rnd() * Math.PI * 2;
|
|
3294
|
+
const r = Math.sqrt(rnd()) * z.radius;
|
|
3295
|
+
renderer.emit("fire", [z.point[0] + Math.cos(a) * r, z.point[1] + 0.05, z.point[2] + Math.sin(a) * r], [0, 1, 0], 2, 1.5);
|
|
3296
|
+
if (z.remaining <= 0) fireZones.splice(i, 1);
|
|
3297
|
+
}
|
|
3298
|
+
if (burningStains.length) {
|
|
3299
|
+
const stains = fluids.stains();
|
|
3300
|
+
for (let i = burningStains.length - 1; i >= 0; i--) {
|
|
3301
|
+
const b = burningStains[i];
|
|
3302
|
+
b.remaining -= dt;
|
|
3303
|
+
b.acc += dt;
|
|
3304
|
+
const st = stains.find((s) => dist3(s.center, b.center) < 0.05);
|
|
3305
|
+
const r = st ? stainRadius(st) : 0.3;
|
|
3306
|
+
if (b.acc >= 0.1) {
|
|
3307
|
+
hitFireAround([b.center[0], b.center[1] + 0.1, b.center[2]], r + 0.2, 800, b.acc);
|
|
3308
|
+
b.acc = 0;
|
|
3309
|
+
}
|
|
3310
|
+
const a = rnd() * Math.PI * 2;
|
|
3311
|
+
const rr = Math.sqrt(rnd()) * r;
|
|
3312
|
+
renderer.emit("fire", [b.center[0] + Math.cos(a) * rr, b.center[1] + 0.05, b.center[2] + Math.sin(a) * rr], [0, 1, 0], 3, 1.8);
|
|
3313
|
+
if (rnd() < 0.2) renderer.emit("smoke", [b.center[0], b.center[1] + 0.3, b.center[2]], [0, 1, 0], 1, 1);
|
|
3314
|
+
if (b.remaining <= 0 || !st) {
|
|
3315
|
+
fluids.clearStains(b.center, r + 0.1);
|
|
3316
|
+
log.add({ channel: "event", type: "stain_burned", text: t("ev.stain_burned"), data: { point: b.center } });
|
|
3317
|
+
burningStains.splice(i, 1);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
let progress = null;
|
|
3323
|
+
let statusLine = "";
|
|
3324
|
+
let statusAt = 0;
|
|
3325
|
+
const say = (msg) => {
|
|
3326
|
+
statusLine = msg;
|
|
3327
|
+
statusAt = simTime;
|
|
3328
|
+
ui?.toast(msg);
|
|
3329
|
+
};
|
|
3330
|
+
const toolName = (id = toolId) => arsenal.name(id);
|
|
3331
|
+
const hitWhere = (r) => {
|
|
3332
|
+
const p = world.part(r.object, r.part);
|
|
3333
|
+
return `${nameOf(r.object)} \u203A ${r.part}${p ? ` (${dStrings.materialNames[p.material] ?? p.material})` : ""}`;
|
|
3334
|
+
};
|
|
3335
|
+
const pushPiece = (objectId, dir, momentum, point) => {
|
|
3336
|
+
if (!rendererPieces.has(objectId)) return;
|
|
3337
|
+
renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
|
|
3338
|
+
};
|
|
3339
|
+
const fireShots = (shots, recoil) => {
|
|
3340
|
+
if (shots.length === 0) return;
|
|
3341
|
+
const w = toolName();
|
|
3342
|
+
const m = muzzle();
|
|
3343
|
+
renderer.emit("flash", m, [aimD[0], aimD[1], aimD[2]], 1, 0);
|
|
3344
|
+
renderer.emit("sparks", m, [aimD[0], aimD[1], aimD[2]], 4, 6);
|
|
3345
|
+
let hits = 0;
|
|
3346
|
+
let first = null;
|
|
3347
|
+
let energy = 0;
|
|
3348
|
+
withoutHeld(() => {
|
|
3349
|
+
for (const s of shots) {
|
|
3350
|
+
energy = Math.max(energy, s.energy);
|
|
3351
|
+
const results = world.shoot(s.origin, s.direction, s.energy, s.caliber, "player");
|
|
3352
|
+
const r0 = results.find((r) => r.part && r.point);
|
|
3353
|
+
let end = [s.origin[0] + s.direction[0] * 40, s.origin[1] + s.direction[1] * 40, s.origin[2] + s.direction[2] * 40];
|
|
3354
|
+
if (r0?.point && r0.part) {
|
|
3355
|
+
hits++;
|
|
3356
|
+
end = r0.point;
|
|
3357
|
+
if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
|
|
3358
|
+
const speed = Math.max(80, s.speed);
|
|
3359
|
+
pushPiece(r0.object, s.direction, 2 * s.energy / speed, r0.point);
|
|
3360
|
+
}
|
|
3361
|
+
if (shots.length <= 3 || rnd() < 0.34) tracer(m, end);
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
player.kick(recoil[0], recoil[1]);
|
|
3365
|
+
const f = first;
|
|
3366
|
+
const d = f ? num(f.distance, 1) : "";
|
|
3367
|
+
let text;
|
|
3368
|
+
if (shots.length === 1) text = t(f ? "act.shoot.hit" : "act.shoot.miss", { weapon: w, energy: num(energy, 0), where: f ? hitWhere(f) : "", dist: d });
|
|
3369
|
+
else text = t("act.shoot.pellets", { weapon: w, n: shots.length, energy: num(energy, 0), hits, where: f ? hitWhere(f) : "", dist: d });
|
|
3370
|
+
action("shoot", text, {
|
|
3371
|
+
objectId: f?.object,
|
|
3372
|
+
itemId: f ? entryOf(f.object)?.so.itemId : void 0,
|
|
3373
|
+
part: f?.part,
|
|
3374
|
+
data: { weapon: toolId, shots: shots.length, hits, energyJ: Math.round(energy), caliberMm: shots[0].caliber, distanceM: f ? Math.round(f.distance * 100) / 100 : null }
|
|
3375
|
+
});
|
|
3376
|
+
};
|
|
3377
|
+
const fireRays = (u) => {
|
|
3378
|
+
const d = new THREE.Vector3(u.direction[0], u.direction[1], u.direction[2]).normalize();
|
|
3379
|
+
const out = [[d.x, d.y, d.z]];
|
|
3380
|
+
if (u.cone) {
|
|
3381
|
+
const a = Math.min(0.2, Math.atan2(u.radius, Math.max(0.1, u.range)));
|
|
3382
|
+
const right = new THREE.Vector3().crossVectors(d, new THREE.Vector3(0, 1, 0));
|
|
3383
|
+
if (right.lengthSq() < 1e-6) right.set(1, 0, 0);
|
|
3384
|
+
right.normalize();
|
|
3385
|
+
const up = new THREE.Vector3().crossVectors(right, d).normalize();
|
|
3386
|
+
for (const [x, y] of [
|
|
3387
|
+
[1, 0],
|
|
3388
|
+
[-1, 0],
|
|
3389
|
+
[0, 1],
|
|
3390
|
+
[0, -1]
|
|
3391
|
+
]) {
|
|
3392
|
+
const r = d.clone().addScaledVector(right, x * a).addScaledVector(up, y * a).normalize();
|
|
3393
|
+
out.push([r.x, r.y, r.z]);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
return out;
|
|
3397
|
+
};
|
|
3398
|
+
const heatRays = (u, type) => {
|
|
3399
|
+
const w = toolName();
|
|
3400
|
+
const cone = u.kind === "flame" ? u.cone : false;
|
|
3401
|
+
const rays = fireRays({ origin: u.origin, direction: u.direction, range: u.range, cone, radius: u.radius });
|
|
3402
|
+
let target = null;
|
|
3403
|
+
const m = muzzle();
|
|
3404
|
+
withoutHeld(() => {
|
|
3405
|
+
for (const d of rays) {
|
|
3406
|
+
const r = world.raycast(u.origin, d, u.range);
|
|
3407
|
+
if (!r) continue;
|
|
3408
|
+
if (!target) target = r;
|
|
3409
|
+
world.hit(r.object, { type, point: r.point, direction: d, temperature: u.temperature, radius: Math.min(u.radius, 0.3), duration: u.duration, source: "player" });
|
|
3410
|
+
if (type === "fire") igniteNear(r.point, 0.25);
|
|
3411
|
+
else renderer.emit("sparks", r.point, r.normal, 2, 4);
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
if (type === "fire") {
|
|
3415
|
+
renderer.emit("fire", m, [u.direction[0], u.direction[1], u.direction[2]], cone ? 5 : 1, Math.max(0.6, u.range * 1.1));
|
|
3416
|
+
if (cone) renderer.emit("smoke", m, [u.direction[0], u.direction[1] + 0.3, u.direction[2]], 1, u.range * 0.5);
|
|
3417
|
+
if (!target && u.direction[1] < -0.2) {
|
|
3418
|
+
const t0 = (0.05 - u.origin[1]) / u.direction[1];
|
|
3419
|
+
if (t0 > 0 && t0 < u.range) igniteNear([u.origin[0] + u.direction[0] * t0, 0.05, u.origin[2] + u.direction[2] * t0], 0.3);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
const tg = target;
|
|
3423
|
+
log.coalesce(
|
|
3424
|
+
`use:${toolId}`,
|
|
3425
|
+
{
|
|
3426
|
+
channel: "action",
|
|
3427
|
+
type,
|
|
3428
|
+
text: tg ? t(type === "fire" ? "act.flame.hit" : "act.heat.hit", { weapon: w, where: hitWhere(tg), temp: num(u.temperature, 0) }) : t(type === "fire" ? "act.flame.air" : "act.heat.air", { weapon: w }),
|
|
3429
|
+
objectId: tg?.object,
|
|
3430
|
+
part: tg?.part,
|
|
3431
|
+
data: { weapon: toolId, temperatureC: u.temperature }
|
|
3432
|
+
},
|
|
3433
|
+
1
|
|
3434
|
+
);
|
|
3435
|
+
};
|
|
3436
|
+
const meleeUse = (u) => {
|
|
3437
|
+
const w = toolName();
|
|
3438
|
+
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
3439
|
+
if (!r) {
|
|
3440
|
+
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: toolId } });
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
world.hitPart(r.object, r.part, { type: u.damage, point: r.point, direction: u.direction, energy: u.energy, radius: u.radius, source: "player" });
|
|
3444
|
+
pushPiece(r.object, u.direction, 2 * u.energy / 6, r.point);
|
|
3445
|
+
action("melee", t("act.melee.hit", { weapon: w, energy: num(u.energy, 0), where: hitWhere(r), dist: num(r.distance, 1) }), {
|
|
3446
|
+
objectId: r.object,
|
|
3447
|
+
itemId: entryOf(r.object)?.so.itemId,
|
|
3448
|
+
part: r.part,
|
|
3449
|
+
data: { weapon: toolId, damage: u.damage, energyJ: u.energy, distanceM: Math.round(r.distance * 100) / 100 }
|
|
3450
|
+
});
|
|
3451
|
+
};
|
|
3452
|
+
const toolWork = (u, dt) => {
|
|
3453
|
+
const w = toolName();
|
|
3454
|
+
const r = rayWorld(u.origin, u.direction, u.reach);
|
|
3455
|
+
if (!r) {
|
|
3456
|
+
progress = null;
|
|
3457
|
+
log.coalesce(`use:${toolId}`, { channel: "action", type: "tool", text: t("act.tool.none", { weapon: w }), data: { weapon: toolId } }, 1);
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
const j = world.nearestJoint(r.object, r.point, 0.45);
|
|
3461
|
+
if (!j) {
|
|
3462
|
+
progress = null;
|
|
3463
|
+
log.coalesce(`use:${toolId}`, { channel: "action", type: "tool", text: t("act.tool.nojoint", { weapon: w, where: hitWhere(r) }), objectId: r.object, part: r.part }, 1);
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
const tool_ = u.tool;
|
|
3467
|
+
const p = world.disassemble(r.object, j.id, tool_, dt);
|
|
3468
|
+
const kind = dStrings.jointKinds[j.kind] ?? j.kind;
|
|
3469
|
+
if (p < 0) {
|
|
3470
|
+
const need = JOINT_DEFAULTS2[j.kind].tools.map((k) => dStrings.toolKinds[k] ?? k).join(", ") || t("tool.none");
|
|
3471
|
+
progress = { label: t("hud.refused", { kind }), value: 0, at: simTime };
|
|
3472
|
+
log.coalesce(
|
|
3473
|
+
`refuse:${r.object}:${j.id}:${toolId}`,
|
|
3474
|
+
{
|
|
3475
|
+
channel: "action",
|
|
3476
|
+
type: "tool_refused",
|
|
3477
|
+
text: t("act.tool.refused", { weapon: w, kind, obj: nameOf(r.object), need }),
|
|
3478
|
+
objectId: r.object,
|
|
3479
|
+
itemId: entryOf(r.object)?.so.itemId,
|
|
3480
|
+
data: { weapon: toolId, joint: j.id, kind: j.kind, needs: JOINT_DEFAULTS2[j.kind].tools }
|
|
3481
|
+
},
|
|
3482
|
+
2
|
|
3483
|
+
);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
progress = { label: `${kind} (${j.id})`, value: p, at: simTime };
|
|
3487
|
+
renderer.emit("sparks", r.point, r.normal, 1, 2);
|
|
3488
|
+
};
|
|
3489
|
+
const extinguishUse = (u) => {
|
|
3490
|
+
const w = toolName();
|
|
3491
|
+
const rays = fireRays({ origin: u.origin, direction: u.direction, range: u.range, cone: true, radius: 0.6 });
|
|
3492
|
+
const m = muzzle();
|
|
3493
|
+
renderer.emit("foam", m, [u.direction[0], u.direction[1], u.direction[2]], 8, u.range * 1.2);
|
|
3494
|
+
const hit = /* @__PURE__ */ new Set();
|
|
3495
|
+
withoutHeld(() => {
|
|
3496
|
+
for (const d of rays) {
|
|
3497
|
+
const r = world.raycast(u.origin, d, u.range);
|
|
3498
|
+
if (!r) continue;
|
|
3499
|
+
hit.add(r.object);
|
|
3500
|
+
world.extinguish(r.object, void 0, u.coolC * Math.max(u.duration, 0.05) * 6);
|
|
3501
|
+
fluids.clearStains(r.point, 0.5);
|
|
3502
|
+
for (let i = burningStains.length - 1; i >= 0; i--) if (Math.hypot(burningStains[i].center[0] - r.point[0], burningStains[i].center[2] - r.point[2]) < 1.2) burningStains.splice(i, 1);
|
|
3503
|
+
for (let i = fireZones.length - 1; i >= 0; i--) if (Math.hypot(fireZones[i].point[0] - r.point[0], fireZones[i].point[2] - r.point[2]) < 1.5) fireZones.splice(i, 1);
|
|
3504
|
+
}
|
|
3505
|
+
});
|
|
3506
|
+
log.coalesce(
|
|
3507
|
+
`use:${toolId}`,
|
|
3508
|
+
{ channel: "action", type: "extinguish", text: hit.size ? t("act.extinguish.hit", { weapon: w, what: [...hit].map(nameOf).join(", ") }) : t("act.extinguish.air", { weapon: w }), data: { objects: [...hit] } },
|
|
3509
|
+
1
|
|
3510
|
+
);
|
|
3511
|
+
};
|
|
3512
|
+
const projectiles = [];
|
|
3513
|
+
const projMeshes = [];
|
|
3514
|
+
const clearProjectileMeshes = () => {
|
|
3515
|
+
for (const m of projMeshes) m.removeFromParent();
|
|
3516
|
+
projMeshes.length = 0;
|
|
3517
|
+
};
|
|
3518
|
+
const throwUse = (u) => {
|
|
3519
|
+
const w = toolName();
|
|
3520
|
+
const molotov = !!u.fire;
|
|
3521
|
+
const mesh = new THREE.Mesh(
|
|
3522
|
+
molotov ? new THREE.CylinderGeometry(0.035, 0.04, 0.2, 8) : new THREE.SphereGeometry(0.045, 8, 6),
|
|
3523
|
+
new THREE.MeshStandardMaterial({ color: molotov ? 3897915 : 4936480, roughness: 0.6, metalness: molotov ? 0 : 0.4 })
|
|
3524
|
+
);
|
|
3525
|
+
mesh.castShadow = true;
|
|
3526
|
+
mesh.position.set(u.origin[0], u.origin[1], u.origin[2]);
|
|
3527
|
+
scene.add(mesh);
|
|
3528
|
+
projMeshes.push(mesh);
|
|
3529
|
+
projectiles.push({
|
|
3530
|
+
mesh,
|
|
3531
|
+
pos: new THREE.Vector3(u.origin[0], u.origin[1], u.origin[2]),
|
|
3532
|
+
vel: new THREE.Vector3(u.velocity[0], u.velocity[1], u.velocity[2]),
|
|
3533
|
+
weapon: toolId,
|
|
3534
|
+
fuse: u.fuse,
|
|
3535
|
+
burstOnImpact: molotov && u.fuse <= 0,
|
|
3536
|
+
energy: u.energy,
|
|
3537
|
+
radius: u.radius,
|
|
3538
|
+
fire: u.fire,
|
|
3539
|
+
age: 0
|
|
3540
|
+
});
|
|
3541
|
+
action("throw_projectile", t(molotov ? "act.throw.molotov" : "act.throw.grenade", { weapon: w, fuse: num(u.fuse, 1) }), { data: { weapon: toolId, fuse: u.fuse } });
|
|
3542
|
+
};
|
|
3543
|
+
const detonate = (p, at, impact) => {
|
|
3544
|
+
const w = toolName(p.weapon);
|
|
3545
|
+
const results = world.explode(at, p.energy, p.radius, "player");
|
|
3546
|
+
action("explode", t("act.explode", { weapon: w, energy: num(p.energy, 0), radius: num(p.radius, 1), n: results.length }), { data: { weapon: p.weapon, energyJ: p.energy, radius: p.radius, point: at } });
|
|
3547
|
+
if (p.fire) {
|
|
3548
|
+
fluids.splash("fuel", [at[0], Math.max(at[1], groundAt(at[0], at[2])), at[2]], 0.5);
|
|
3549
|
+
fireZones.push({ point: [at[0], Math.max(0.02, groundAt(at[0], at[2])), at[2]], radius: p.fire.radius, temperature: p.fire.temperature, remaining: p.fire.seconds, acc: 0 });
|
|
3550
|
+
renderer.emit("fire", at, [0, 1, 0], 24, 3);
|
|
3551
|
+
action("molotov_fire", t("act.molotov_fire", { weapon: w, seconds: num(p.fire.seconds, 0), radius: num(p.fire.radius, 1) }), { data: { weapon: p.weapon, impact } });
|
|
3552
|
+
}
|
|
3553
|
+
};
|
|
3554
|
+
const updateProjectiles = (dt) => {
|
|
3555
|
+
for (let i = projectiles.length - 1; i >= 0; i--) {
|
|
3556
|
+
const p = projectiles[i];
|
|
3557
|
+
p.age += dt;
|
|
3558
|
+
p.vel.y -= 9.81 * dt;
|
|
3559
|
+
const speed = p.vel.length();
|
|
3560
|
+
const step2 = speed * dt;
|
|
3561
|
+
let burst = false;
|
|
3562
|
+
if (step2 > 1e-6) {
|
|
3563
|
+
const d = [p.vel.x / speed, p.vel.y / speed, p.vel.z / speed];
|
|
3564
|
+
const r = withoutHeld(() => world.raycast(v3(p.pos), d, step2 + 0.05));
|
|
3565
|
+
if (r && r.distance <= step2 + 0.04) {
|
|
3566
|
+
p.pos.set(r.point[0] - d[0] * 0.03, r.point[1] - d[1] * 0.03, r.point[2] - d[2] * 0.03);
|
|
3567
|
+
if (p.burstOnImpact) burst = true;
|
|
3568
|
+
else {
|
|
3569
|
+
const n = r.normal;
|
|
3570
|
+
const vn = p.vel.x * n[0] + p.vel.y * n[1] + p.vel.z * n[2];
|
|
3571
|
+
p.vel.x = (p.vel.x - 2 * vn * n[0]) * 0.35;
|
|
3572
|
+
p.vel.y = (p.vel.y - 2 * vn * n[1]) * 0.35;
|
|
3573
|
+
p.vel.z = (p.vel.z - 2 * vn * n[2]) * 0.35;
|
|
3574
|
+
}
|
|
3575
|
+
} else p.pos.addScaledVector(p.vel, dt);
|
|
3576
|
+
}
|
|
3577
|
+
const g = groundAt(p.pos.x, p.pos.z) + 0.045;
|
|
3578
|
+
if (p.pos.y < g) {
|
|
3579
|
+
p.pos.y = g;
|
|
3580
|
+
if (p.burstOnImpact) burst = true;
|
|
3581
|
+
else {
|
|
3582
|
+
p.vel.y = Math.abs(p.vel.y) * 0.35;
|
|
3583
|
+
p.vel.x *= 0.7;
|
|
3584
|
+
p.vel.z *= 0.7;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
p.mesh.position.copy(p.pos);
|
|
3588
|
+
p.mesh.rotation.x += dt * 8;
|
|
3589
|
+
p.fuse -= dt;
|
|
3590
|
+
if (burst || !p.burstOnImpact && p.fuse <= 0 || p.age > 30) {
|
|
3591
|
+
detonate(p, v3(p.pos), burst);
|
|
3592
|
+
p.mesh.removeFromParent();
|
|
3593
|
+
projMeshes.splice(projMeshes.indexOf(p.mesh), 1);
|
|
3594
|
+
projectiles.splice(i, 1);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
};
|
|
3598
|
+
const useTool = (dt, held) => {
|
|
3599
|
+
aim();
|
|
3600
|
+
const cat = tool.def.category;
|
|
3601
|
+
if (cat === "hand") return;
|
|
3602
|
+
const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
3603
|
+
if (res.status === "empty") {
|
|
3604
|
+
if (!tool.reloading && tool.reserve > 0) tool.reload();
|
|
3605
|
+
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
if (res.status !== "ok") {
|
|
3609
|
+
if (res.status === "reloading") say(t("hud.reloading"));
|
|
3610
|
+
else if (res.status === "no_fuel") say(t("hud.no_fuel"));
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
if (res.shots.length) fireShots(res.shots, res.recoil);
|
|
3614
|
+
for (const u of res.uses) {
|
|
3615
|
+
switch (u.kind) {
|
|
3616
|
+
case "melee":
|
|
3617
|
+
meleeUse(u);
|
|
3618
|
+
break;
|
|
3619
|
+
case "flame":
|
|
3620
|
+
heatRays(u, "fire");
|
|
3621
|
+
break;
|
|
3622
|
+
case "heat":
|
|
3623
|
+
heatRays(u, "heat");
|
|
3624
|
+
break;
|
|
3625
|
+
case "throw":
|
|
3626
|
+
throwUse(u);
|
|
3627
|
+
break;
|
|
3628
|
+
case "tool":
|
|
3629
|
+
toolWork(u, dt);
|
|
3630
|
+
break;
|
|
3631
|
+
case "extinguish":
|
|
3632
|
+
extinguishUse(u);
|
|
3633
|
+
break;
|
|
3634
|
+
default:
|
|
3635
|
+
break;
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
};
|
|
3639
|
+
const setTool = (id) => {
|
|
3640
|
+
if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
|
|
3641
|
+
tool.release();
|
|
3642
|
+
toolId = id;
|
|
3643
|
+
const i = loadout.indexOf(id);
|
|
3644
|
+
if (i >= 0) toolIndex = i;
|
|
3645
|
+
tool = toolState(id);
|
|
3646
|
+
progress = null;
|
|
3647
|
+
action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
|
|
3648
|
+
fire("tool", id);
|
|
3649
|
+
};
|
|
3650
|
+
const cycleTool = (dir) => {
|
|
3651
|
+
if (!loadout.length) return;
|
|
3652
|
+
toolIndex = (toolIndex + dir + loadout.length) % loadout.length;
|
|
3653
|
+
setTool(loadout[toolIndex]);
|
|
3654
|
+
};
|
|
3655
|
+
let heldEntry = null;
|
|
3656
|
+
let heldPiece = null;
|
|
3657
|
+
let heldName = "";
|
|
3658
|
+
let heldYaw = 0;
|
|
3659
|
+
let heldTilt = 0;
|
|
3660
|
+
let tiltKey = 0;
|
|
3661
|
+
const heldLocal = new THREE.Vector3();
|
|
3662
|
+
const heldVel = [0, 0, 0];
|
|
3663
|
+
let useItemPlayer = null;
|
|
3664
|
+
let useItemName = "";
|
|
3665
|
+
const releaseHeld = () => {
|
|
3666
|
+
if (heldPiece) {
|
|
3667
|
+
heldPiece.held = false;
|
|
3668
|
+
heldPiece.sleeping = false;
|
|
3669
|
+
heldPiece.velocity = [heldVel[0], heldVel[1], heldVel[2]];
|
|
3670
|
+
}
|
|
3671
|
+
useItemPlayer?.cancel();
|
|
3672
|
+
useItemPlayer = null;
|
|
3673
|
+
heldEntry = null;
|
|
3674
|
+
heldPiece = null;
|
|
3675
|
+
heldTilt = 0;
|
|
3676
|
+
};
|
|
3677
|
+
const grab = () => {
|
|
3678
|
+
if (heldPiece) {
|
|
3679
|
+
const name = heldName;
|
|
3680
|
+
releaseHeld();
|
|
3681
|
+
action("drop", t("act.drop", { name }));
|
|
3682
|
+
fire("grab", { held: false });
|
|
3683
|
+
return true;
|
|
3684
|
+
}
|
|
3685
|
+
aim();
|
|
3686
|
+
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
|
|
3687
|
+
if (!r) {
|
|
3688
|
+
say(t("hud.nothing_to_grab"));
|
|
3689
|
+
return false;
|
|
3690
|
+
}
|
|
3691
|
+
const piece = rendererPieces.get(r.object);
|
|
3692
|
+
if (!piece) {
|
|
3693
|
+
say(t("hud.cannot_grab", { name: nameOf(r.object) }));
|
|
3694
|
+
return false;
|
|
3695
|
+
}
|
|
3696
|
+
const kg = piece.mass;
|
|
3697
|
+
if (kg > maxCarry) {
|
|
3698
|
+
say(t("hud.too_heavy", { name: nameOf(r.object), kg: num(kg, 0) }));
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
heldPiece = piece;
|
|
3702
|
+
heldEntry = entries.get(r.object) ?? null;
|
|
3703
|
+
heldName = nameOf(r.object);
|
|
3704
|
+
piece.held = true;
|
|
3705
|
+
piece.sleeping = false;
|
|
3706
|
+
const box2 = new THREE.Box3().setFromObject(piece.root);
|
|
3707
|
+
const c = box2.getCenter(new THREE.Vector3());
|
|
3708
|
+
piece.root.updateMatrixWorld(true);
|
|
3709
|
+
heldLocal.copy(c).sub(piece.root.position);
|
|
3710
|
+
heldLocal.applyQuaternion(piece.root.quaternion.clone().invert());
|
|
3711
|
+
heldYaw = 0;
|
|
3712
|
+
heldTilt = 0;
|
|
3713
|
+
heldVel[0] = heldVel[1] = heldVel[2] = 0;
|
|
3714
|
+
action("grab", t("act.grab", { name: heldName, kg: num(kg, 1) }), { objectId: r.object, itemId: entryOf(r.object)?.so.itemId, data: { massKg: Math.round(kg * 10) / 10 } });
|
|
3715
|
+
fire("grab", { held: true, objectId: r.object });
|
|
3716
|
+
return true;
|
|
3717
|
+
};
|
|
3718
|
+
const throwHeld = (speed = 9) => {
|
|
3719
|
+
if (!heldPiece) return false;
|
|
3720
|
+
const piece = heldPiece;
|
|
3721
|
+
const name = heldName;
|
|
3722
|
+
const id = piece.objectId;
|
|
3723
|
+
aim();
|
|
3724
|
+
releaseHeld();
|
|
3725
|
+
piece.velocity = [0, 0, 0];
|
|
3726
|
+
renderer.impulse(id, [aimD[0] * speed * piece.mass, (aimD[1] * speed + 1.2) * piece.mass, aimD[2] * speed * piece.mass]);
|
|
3727
|
+
action("throw", t("act.throw", { name, speed: num(speed, 1) }), { objectId: id, itemId: entryOf(id)?.so.itemId, data: { speedMs: speed, massKg: piece.mass } });
|
|
3728
|
+
fire("throw", { objectId: id, speed });
|
|
3729
|
+
return true;
|
|
3730
|
+
};
|
|
3731
|
+
const followHeld = (dt) => {
|
|
3732
|
+
const piece = heldPiece;
|
|
3733
|
+
if (!piece) return;
|
|
3734
|
+
camera.updateMatrixWorld(true);
|
|
3735
|
+
const root = piece.root;
|
|
3736
|
+
let pos;
|
|
3737
|
+
const targetQ = tmpQ;
|
|
3738
|
+
if (useItemPlayer) {
|
|
3739
|
+
const pose = useItemPlayer.pose();
|
|
3740
|
+
const handQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
3741
|
+
pos = tmpV.set(pose.position[0], pose.position[1], pose.position[2]).applyQuaternion(handQ).add(camera.position);
|
|
3742
|
+
const eq = new THREE.Quaternion().setFromEuler(new THREE.Euler(pose.rotation[0], pose.rotation[1], pose.rotation[2], "XYZ"));
|
|
3743
|
+
targetQ.copy(handQ).multiply(eq);
|
|
3744
|
+
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
3745
|
+
} else {
|
|
3746
|
+
pos = tmpV.set(0.05, -0.28, -1.15).applyMatrix4(camera.matrixWorld);
|
|
3747
|
+
targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
|
|
3748
|
+
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
3749
|
+
}
|
|
3750
|
+
const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
|
|
3751
|
+
const px = root.position.x;
|
|
3752
|
+
const py = root.position.y;
|
|
3753
|
+
const pz = root.position.z;
|
|
3754
|
+
root.quaternion.slerp(targetQ, Math.min(1, k * 0.9));
|
|
3755
|
+
const off2 = tmpV2.copy(heldLocal).applyQuaternion(root.quaternion);
|
|
3756
|
+
root.position.set(root.position.x + (pos.x - off2.x - root.position.x) * k, root.position.y + (pos.y - off2.y - root.position.y) * k, root.position.z + (pos.z - off2.z - root.position.z) * k);
|
|
3757
|
+
if (dt > 0) {
|
|
3758
|
+
heldVel[0] = (root.position.x - px) / dt;
|
|
3759
|
+
heldVel[1] = (root.position.y - py) / dt;
|
|
3760
|
+
heldVel[2] = (root.position.z - pz) / dt;
|
|
3761
|
+
}
|
|
3762
|
+
root.updateMatrixWorld(true);
|
|
3763
|
+
};
|
|
3764
|
+
const useItem = (useId) => {
|
|
3765
|
+
const e = heldEntry;
|
|
3766
|
+
if (!e?.containerId) return false;
|
|
3767
|
+
const def = itemUseDefs.find((u) => u.id === useId);
|
|
3768
|
+
if (!def) return false;
|
|
3769
|
+
const c = fluids.container(e.containerId);
|
|
3770
|
+
if (!c) return false;
|
|
3771
|
+
if (def.liquids && def.liquids.length > 0 && (!c.liquid || !def.liquids.includes(c.liquid))) return false;
|
|
3772
|
+
if ((useId === "drink" || useId === "sip" || useId === "chug" || useId === "pour" || useId === "pour_out") && c.liters <= 1e-6) {
|
|
3773
|
+
say(t("hud.empty_container", { name: e.name }));
|
|
3774
|
+
return false;
|
|
3775
|
+
}
|
|
3776
|
+
if (useItemPlayer && !useItemPlayer.done) return false;
|
|
3777
|
+
useItemPlayer = createItemUse(fluids, e.containerId, def);
|
|
3778
|
+
useItemName = def.id;
|
|
3779
|
+
action("use_item", t("act.use_item", { name: e.name, use: S.text[`use.${def.id}`] ?? def.id }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { use: def.id } });
|
|
3780
|
+
return true;
|
|
3781
|
+
};
|
|
3782
|
+
const updateItemUse = (dt) => {
|
|
3783
|
+
if (!useItemPlayer) return;
|
|
3784
|
+
useItemPlayer.update(dt);
|
|
3785
|
+
if (useItemPlayer.done) {
|
|
3786
|
+
useItemPlayer = null;
|
|
3787
|
+
useItemName = "";
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
const TAP_ID = "room_tap";
|
|
3791
|
+
const TAP_RATE = 0.25;
|
|
3792
|
+
fluids.addTap({ id: TAP_ID, position: arena.tapSpout, liquid: "water", rate: TAP_RATE, temperature: 16, on: false });
|
|
3793
|
+
const isTapOn = () => fluids.taps().some((x) => x.id === TAP_ID && x.on);
|
|
3794
|
+
const toggleTap = (on = !isTapOn()) => {
|
|
3795
|
+
fluids.setTap(TAP_ID, on);
|
|
3796
|
+
action("tap", t(on ? "act.tap.on" : "act.tap.off"));
|
|
3797
|
+
return on;
|
|
3798
|
+
};
|
|
3799
|
+
const lastImpact = /* @__PURE__ */ new Map();
|
|
3800
|
+
const MIN_LANDING_LOG = 3;
|
|
3801
|
+
const updateImpacts = () => {
|
|
3802
|
+
const check = (piece) => {
|
|
3803
|
+
if (piece.held || piece.sleeping) return;
|
|
3804
|
+
const v = piece.velocity;
|
|
3805
|
+
const speed = Math.hypot(v[0], v[1], v[2]);
|
|
3806
|
+
if (speed < 3.5) return;
|
|
3807
|
+
if ((lastImpact.get(piece.objectId) ?? -9) > simTime - 0.25) return;
|
|
3808
|
+
const dir = [v[0] / speed, v[1] / speed, v[2] / speed];
|
|
3809
|
+
const c = tmpV.copy(new THREE.Vector3(0, 0, 0));
|
|
3810
|
+
void c;
|
|
3811
|
+
const box2 = tmpBox.setFromObject(piece.root);
|
|
3812
|
+
const centre = box2.getCenter(tmpV);
|
|
3813
|
+
const size = box2.getSize(tmpV2);
|
|
3814
|
+
const reach = Math.max(size.x, size.y, size.z) * 0.5;
|
|
3815
|
+
const origin = [centre.x + dir[0] * (reach + 0.02), centre.y + dir[1] * (reach + 0.02), centre.z + dir[2] * (reach + 0.02)];
|
|
3816
|
+
const r = withoutHeld(() => world.raycast(origin, dir, speed * FIXED_DT * 2 + 0.06));
|
|
3817
|
+
if (!r || r.object === piece.objectId) return;
|
|
3818
|
+
const energy = 0.5 * piece.mass * speed * speed;
|
|
3819
|
+
lastImpact.set(piece.objectId, simTime);
|
|
3820
|
+
world.hitPart(r.object, r.part, { type: "blunt", point: r.point, direction: dir, energy, radius: 0.08, source: "thrown" });
|
|
3821
|
+
if (world.has(piece.objectId)) world.hit(piece.objectId, { type: "blunt", point: r.point, direction: [-dir[0], -dir[1], -dir[2]], energy: energy * 0.4, radius: 0.08, source: "thrown" });
|
|
3822
|
+
pushPiece(r.object, dir, piece.mass * speed * 0.5, r.point);
|
|
3823
|
+
piece.velocity = [-v[0] * 0.25, Math.abs(v[1]) * 0.3 + 0.5, -v[2] * 0.25];
|
|
3824
|
+
action("impact", t("act.impact", { name: nameOf(piece.objectId), where: hitWhere(r), energy: num(energy, 0), speed: num(speed, 1) }), {
|
|
3825
|
+
objectId: piece.objectId,
|
|
3826
|
+
itemId: entryOf(piece.objectId)?.so.itemId,
|
|
3827
|
+
data: { energyJ: Math.round(energy), speedMs: Math.round(speed * 10) / 10, target: r.object, targetPart: r.part }
|
|
3828
|
+
});
|
|
3829
|
+
};
|
|
3830
|
+
const land = (piece) => {
|
|
3831
|
+
const vy = piece.velocity[1];
|
|
3832
|
+
const prev = prevVy.get(piece.objectId) ?? 0;
|
|
3833
|
+
prevVy.set(piece.objectId, vy);
|
|
3834
|
+
if (piece.held || prev > -MIN_LANDING_LOG || vy < prev + 2 || !world.has(piece.objectId)) return;
|
|
3835
|
+
const energy = 0.5 * piece.mass * prev * prev;
|
|
3836
|
+
action("landing", t("act.landing", { name: nameOf(piece.objectId), speed: num(-prev, 1), energy: num(energy, energy < 100 ? 1 : 0) }), {
|
|
3837
|
+
objectId: piece.objectId,
|
|
3838
|
+
itemId: entryOf(piece.objectId)?.so.itemId,
|
|
3839
|
+
data: { energyJ: Math.round(energy * 10) / 10, speedMs: Math.round(-prev * 10) / 10 }
|
|
3840
|
+
});
|
|
3841
|
+
};
|
|
3842
|
+
for (const e of entries.values()) if (e.piece) {
|
|
3843
|
+
check(e.piece);
|
|
3844
|
+
land(e.piece);
|
|
3845
|
+
}
|
|
3846
|
+
for (const p of loose) {
|
|
3847
|
+
check(p);
|
|
3848
|
+
land(p);
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
const prevVy = /* @__PURE__ */ new Map();
|
|
3852
|
+
const tmpBox = new THREE.Box3();
|
|
3853
|
+
const onKey = (code, down, e) => {
|
|
3854
|
+
if (!down) {
|
|
3855
|
+
if (code === "KeyZ") tiltKey = 0;
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
const digit = /^Digit(\d)$/.exec(code);
|
|
3859
|
+
if (digit && !e.ctrlKey && !e.metaKey) {
|
|
3860
|
+
const n = (Number(digit[1]) + 9) % 10;
|
|
3861
|
+
const page = Math.floor(toolIndex / 10);
|
|
3862
|
+
const idx = page * 10 + n;
|
|
3863
|
+
if (idx < loadout.length) setTool(loadout[idx]);
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
switch (code) {
|
|
3867
|
+
case "KeyE":
|
|
3868
|
+
grab();
|
|
3869
|
+
break;
|
|
3870
|
+
case "KeyG":
|
|
3871
|
+
throwHeld();
|
|
3872
|
+
break;
|
|
3873
|
+
case "KeyR":
|
|
3874
|
+
if (heldPiece) heldYaw += Math.PI / 8;
|
|
3875
|
+
else {
|
|
3876
|
+
tool.reload();
|
|
3877
|
+
say(t("hud.reloading"));
|
|
3878
|
+
}
|
|
3879
|
+
break;
|
|
3880
|
+
case "KeyQ":
|
|
3881
|
+
tool.cycleMode();
|
|
3882
|
+
break;
|
|
3883
|
+
case "KeyZ":
|
|
3884
|
+
tiltKey = 1;
|
|
3885
|
+
break;
|
|
3886
|
+
case "KeyF":
|
|
3887
|
+
if (heldEntry?.uses[0]) useItem(heldEntry.uses[0]);
|
|
3888
|
+
break;
|
|
3889
|
+
case "KeyC":
|
|
3890
|
+
if (heldEntry?.uses[1]) useItem(heldEntry.uses[1]);
|
|
3891
|
+
break;
|
|
3892
|
+
case "KeyV":
|
|
3893
|
+
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
3894
|
+
else toggleTap();
|
|
3895
|
+
break;
|
|
3896
|
+
case "KeyT":
|
|
3897
|
+
toggleTap();
|
|
3898
|
+
break;
|
|
3899
|
+
case "KeyH": {
|
|
3900
|
+
const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
|
|
3901
|
+
if (w) equip(w);
|
|
3902
|
+
break;
|
|
3903
|
+
}
|
|
3904
|
+
case "BracketRight":
|
|
3905
|
+
toolIndex = Math.min(loadout.length - 1, (Math.floor(toolIndex / 10) + 1) * 10);
|
|
3906
|
+
setTool(loadout[toolIndex]);
|
|
3907
|
+
break;
|
|
3908
|
+
case "BracketLeft":
|
|
3909
|
+
toolIndex = Math.max(0, (Math.floor(toolIndex / 10) - 1) * 10);
|
|
3910
|
+
setTool(loadout[toolIndex]);
|
|
3911
|
+
break;
|
|
3912
|
+
case "Tab":
|
|
3913
|
+
ui?.togglePanels();
|
|
3914
|
+
break;
|
|
3915
|
+
default:
|
|
3916
|
+
break;
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
const equip = (weaponId) => {
|
|
3920
|
+
if (!arsenal.weapons.has(weaponId)) return;
|
|
3921
|
+
if (!loadout.includes(weaponId)) loadout.push(weaponId);
|
|
3922
|
+
setTool(weaponId);
|
|
3923
|
+
};
|
|
3924
|
+
const inspectTarget = () => {
|
|
3925
|
+
aim();
|
|
3926
|
+
return rayWorld(aimO, aimD, 14);
|
|
3927
|
+
};
|
|
3928
|
+
const inspectInfo = () => {
|
|
3929
|
+
const r = inspectTarget();
|
|
3930
|
+
if (!r) return null;
|
|
3931
|
+
const st = world.state(r.object);
|
|
3932
|
+
const p = world.part(r.object, r.part);
|
|
3933
|
+
if (!st || !p) return null;
|
|
3934
|
+
const e = entryOf(r.object);
|
|
3935
|
+
const joints = st.joints.filter((j) => j.a === r.part || j.b === r.part).map((j) => ({
|
|
3936
|
+
id: j.id,
|
|
3937
|
+
kind: dStrings.jointKinds[j.kind] ?? j.kind,
|
|
3938
|
+
tools: JOINT_DEFAULTS2[j.kind].tools.map((k) => dStrings.toolKinds[k] ?? k),
|
|
3939
|
+
open: j.open,
|
|
3940
|
+
progress: j.progress
|
|
3941
|
+
}));
|
|
3942
|
+
const ext = p;
|
|
3943
|
+
return {
|
|
3944
|
+
object: nameOf(r.object),
|
|
3945
|
+
objectId: r.object,
|
|
3946
|
+
part: r.part,
|
|
3947
|
+
material: S.text[`material.${p.material}`] ?? dStrings.materialNames[p.material] ?? p.material,
|
|
3948
|
+
status: dStrings.partStatus[p.status] ?? p.status,
|
|
3949
|
+
hp: p.hp,
|
|
3950
|
+
maxHp: p.maxHp,
|
|
3951
|
+
temperature: p.temperature,
|
|
3952
|
+
burning: p.burning,
|
|
3953
|
+
char: p.char,
|
|
3954
|
+
melt: p.melt,
|
|
3955
|
+
liters: p.liters,
|
|
3956
|
+
dents: p.dents.length,
|
|
3957
|
+
holes: p.holes.length,
|
|
3958
|
+
cracks: p.cracks.length,
|
|
3959
|
+
mass: p.mass,
|
|
3960
|
+
load: ext.load,
|
|
3961
|
+
stress: ext.stress,
|
|
3962
|
+
joints,
|
|
3963
|
+
note: e?.item?.note,
|
|
3964
|
+
distance: r.distance
|
|
3965
|
+
};
|
|
3966
|
+
};
|
|
3967
|
+
let timeScale = 1;
|
|
3968
|
+
let fastForward = false;
|
|
3969
|
+
let hudAcc = 1;
|
|
3970
|
+
let qualityName = quality;
|
|
3971
|
+
const syncContainers = (sdt) => {
|
|
3972
|
+
for (const e of entries.values()) {
|
|
3973
|
+
if (!e.containerId) continue;
|
|
3974
|
+
const host = hostOf(e);
|
|
3975
|
+
host.root.updateMatrixWorld(true);
|
|
3976
|
+
const q = host.root.getWorldQuaternion(tmpQ);
|
|
3977
|
+
const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host.root.matrixWorld);
|
|
3978
|
+
const vel = sdt > 0 ? [(p.x - e.last[0]) / sdt, (p.y - e.last[1]) / sdt, (p.z - e.last[2]) / sdt] : void 0;
|
|
3979
|
+
e.last = [p.x, p.y, p.z];
|
|
3980
|
+
fluids.setTransform(e.containerId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w], vel);
|
|
3981
|
+
}
|
|
3982
|
+
};
|
|
3983
|
+
const syncLiters = () => {
|
|
3984
|
+
for (const e of entries.values()) {
|
|
3985
|
+
if (!e.containerId || !e.fluidPart) continue;
|
|
3986
|
+
const c = fluids.container(e.containerId);
|
|
3987
|
+
if (!c) continue;
|
|
3988
|
+
const host = hostOf(e);
|
|
3989
|
+
const part = world.part(host.objectId, e.fluidPart);
|
|
3990
|
+
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host.objectId, e.fluidPart, c.liters);
|
|
3991
|
+
}
|
|
3992
|
+
};
|
|
3993
|
+
const stepSim = (dt) => {
|
|
3994
|
+
simTime += dt;
|
|
3995
|
+
const reps = fastForward ? 10 : 1;
|
|
3996
|
+
updateAnimations(dt);
|
|
3997
|
+
updateItemUse(dt);
|
|
3998
|
+
updateProjectiles(dt);
|
|
3999
|
+
syncContainers(dt);
|
|
4000
|
+
for (let i = 0; i < reps; i++) {
|
|
4001
|
+
world.update(dt);
|
|
4002
|
+
if (i === 0) updateFires(dt);
|
|
4003
|
+
fluids.update(dt);
|
|
4004
|
+
}
|
|
4005
|
+
syncLiters();
|
|
4006
|
+
renderer.update(dt);
|
|
4007
|
+
updateImpacts();
|
|
4008
|
+
fluidView.update(dt);
|
|
4009
|
+
for (const s of tools.values()) s.update(dt);
|
|
4010
|
+
for (const tr of tracers) {
|
|
4011
|
+
if (tr.life > 0) {
|
|
4012
|
+
tr.life -= dt;
|
|
4013
|
+
if (tr.life <= 0) tr.line.visible = false;
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
flush();
|
|
4017
|
+
};
|
|
4018
|
+
const update = (dt) => {
|
|
4019
|
+
player.update(dt);
|
|
4020
|
+
if (tiltKey || heldTilt > 0) {
|
|
4021
|
+
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
4022
|
+
}
|
|
4023
|
+
if (useHeld) {
|
|
4024
|
+
if (tool.def.category === "hand") {
|
|
4025
|
+
if (useFirst) grab();
|
|
4026
|
+
} else useTool(dt, !useFirst);
|
|
4027
|
+
useFirst = false;
|
|
4028
|
+
}
|
|
4029
|
+
followHeld(dt);
|
|
4030
|
+
stepSim(dt * timeScale);
|
|
4031
|
+
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
4032
|
+
hudAcc += dt;
|
|
4033
|
+
if (hudAcc >= 0.1 && ui) {
|
|
4034
|
+
hudAcc = 0;
|
|
4035
|
+
ui.hud(hudState());
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
const hudState = () => {
|
|
4039
|
+
const page = Math.floor(toolIndex / 10);
|
|
4040
|
+
const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
|
|
4041
|
+
const ammo = tool.def.action.kind === "firearm" ? `${tool.ammo}/${tool.capacity}${tool.reserve ? ` \xB7 ${tool.reserve}` : ""}` : tool.def.action.kind === "hand" ? "" : tool.def.action.kind === "melee" || tool.def.action.kind === "tool" ? "" : `${num(tool.ammo, 0)}`;
|
|
4042
|
+
return {
|
|
4043
|
+
toolName: toolName(),
|
|
4044
|
+
toolId,
|
|
4045
|
+
ammo,
|
|
4046
|
+
slots,
|
|
4047
|
+
slot: toolIndex - page * 10,
|
|
4048
|
+
page: page + 1,
|
|
4049
|
+
pages: Math.max(1, Math.ceil(loadout.length / 10)),
|
|
4050
|
+
status: simTime - statusAt < 2 ? statusLine : "",
|
|
4051
|
+
progress: progress ? { label: progress.label, value: progress.value } : null,
|
|
4052
|
+
held: heldEntry || heldPiece ? { name: heldName, uses: (heldEntry?.uses ?? []).map((u, i) => ({ key: ["F", "C", "V"][i] ?? "", label: S.text[`use.${u}`] ?? u })), weapon: heldEntry?.item?.weapon ? arsenal.name(heldEntry.item.weapon) : void 0, using: useItemName } : null,
|
|
4053
|
+
inspect: inspectInfo(),
|
|
4054
|
+
animations: animationsOf(),
|
|
4055
|
+
locked: player.locked,
|
|
4056
|
+
timeScale,
|
|
4057
|
+
fast: fastForward,
|
|
4058
|
+
tap: isTapOn(),
|
|
4059
|
+
quality: qualityName,
|
|
4060
|
+
objects: [...entries.values()].filter((e) => !e.arena).length
|
|
4061
|
+
};
|
|
4062
|
+
};
|
|
4063
|
+
const playing = [];
|
|
4064
|
+
const restPose = /* @__PURE__ */ new WeakMap();
|
|
4065
|
+
const ZERO = [0, 0, 0];
|
|
4066
|
+
const animTarget = () => {
|
|
4067
|
+
if (heldEntry) return heldEntry;
|
|
4068
|
+
const r = inspectTarget();
|
|
4069
|
+
return r ? entryOf(r.object) : void 0;
|
|
4070
|
+
};
|
|
4071
|
+
const poseAt = (clip, id, time) => {
|
|
4072
|
+
const pick = (k) => id === null ? k : k.parts?.[id];
|
|
4073
|
+
const pts = clip.keys.map((k) => ({ t: k.t, position: pick(k)?.position ?? ZERO, rotation: pick(k)?.rotation ?? ZERO }));
|
|
4074
|
+
if (pts.length === 0 || pts[0].t > 0) pts.unshift({ t: 0, position: ZERO, rotation: ZERO });
|
|
4075
|
+
let a = pts[0];
|
|
4076
|
+
let b = pts[pts.length - 1];
|
|
4077
|
+
if (time >= b.t) a = b;
|
|
4078
|
+
else for (let i = 0; i < pts.length - 1; i++) if (time >= pts[i].t && time < pts[i + 1].t) [a, b] = [pts[i], pts[i + 1]];
|
|
4079
|
+
const f = a === b || b.t <= a.t ? 0 : (time - a.t) / (b.t - a.t);
|
|
4080
|
+
const mix = (x, y) => [x[0] + (y[0] - x[0]) * f, x[1] + (y[1] - x[1]) * f, x[2] + (y[2] - x[2]) * f];
|
|
4081
|
+
return { position: mix(a.position, b.position), rotation: mix(a.rotation, b.rotation) };
|
|
4082
|
+
};
|
|
4083
|
+
const applyClip = (pl) => {
|
|
4084
|
+
for (const [id, obj] of pl.targets) {
|
|
4085
|
+
const rest = restPose.get(obj);
|
|
4086
|
+
if (!rest) continue;
|
|
4087
|
+
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
4088
|
+
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
4089
|
+
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
4090
|
+
}
|
|
4091
|
+
};
|
|
4092
|
+
const updateAnimations = (dt) => {
|
|
4093
|
+
for (let i = playing.length - 1; i >= 0; i--) {
|
|
4094
|
+
const pl = playing[i];
|
|
4095
|
+
pl.t = Math.min(pl.clip.duration, pl.t + dt);
|
|
4096
|
+
applyClip(pl);
|
|
4097
|
+
const evs = pl.clip.events ?? [];
|
|
4098
|
+
while (pl.fired < evs.length && evs[pl.fired].t <= pl.t) {
|
|
4099
|
+
const ev = evs[pl.fired++];
|
|
4100
|
+
action("animation_event", t("act.anim_event", { name: pl.entry.name, clip: pl.clip.id, type: ev.type }), { objectId: pl.entry.so.objectId, itemId: pl.entry.so.itemId, data: { clip: pl.clip.id, type: ev.type, at: ev.t, ...ev.data ?? {} } });
|
|
4101
|
+
const key = ev.type === "sound" ? ev.data?.key : void 0;
|
|
4102
|
+
if (typeof key === "string") options.onSound?.(key, v3(pl.entry.so.root.getWorldPosition(tmpV)), 1);
|
|
4103
|
+
}
|
|
4104
|
+
if (pl.t >= pl.clip.duration) {
|
|
4105
|
+
playing.splice(i, 1);
|
|
4106
|
+
action("animation_end", t("act.anim_end", { name: pl.entry.name, clip: pl.clip.id }), { objectId: pl.entry.so.objectId, itemId: pl.entry.so.itemId, data: { clip: pl.clip.id } });
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
};
|
|
4110
|
+
const animationsOf = () => {
|
|
4111
|
+
const e = animTarget();
|
|
4112
|
+
if (!e || e.arena) return null;
|
|
4113
|
+
const clips = (e.item?.animations ?? []).map((c) => ({ id: c.id, duration: c.duration }));
|
|
4114
|
+
return { objectId: e.so.objectId, itemId: e.so.itemId, name: e.name, clips, playing: playing.find((p) => p.entry === e)?.clip.id ?? null };
|
|
4115
|
+
};
|
|
4116
|
+
const playAnimation = (clipId, objectId) => {
|
|
4117
|
+
const e = objectId ? entries.get(objectId) : animTarget();
|
|
4118
|
+
const clip = e?.item?.animations?.find((c) => c.id === clipId);
|
|
4119
|
+
if (!e || !clip) return false;
|
|
4120
|
+
for (let i = playing.length - 1; i >= 0; i--) if (playing[i].entry === e && playing[i].clip.id === clipId) playing.splice(i, 1);
|
|
4121
|
+
const targets = /* @__PURE__ */ new Map();
|
|
4122
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4123
|
+
for (const k of clip.keys) for (const id of Object.keys(k.parts ?? {})) ids.add(id);
|
|
4124
|
+
const grab2 = (key, obj) => {
|
|
4125
|
+
if (!obj) return;
|
|
4126
|
+
if (!restPose.has(obj)) restPose.set(obj, { p: [obj.position.x, obj.position.y, obj.position.z], r: [obj.rotation.x, obj.rotation.y, obj.rotation.z] });
|
|
4127
|
+
targets.set(key, obj);
|
|
4128
|
+
};
|
|
4129
|
+
if (clip.keys.some((k) => k.position || k.rotation)) grab2("", e.so.root);
|
|
4130
|
+
for (const id of ids) grab2(id, e.so.root.getObjectByName(id) ?? void 0);
|
|
4131
|
+
const pl = { entry: e, clip, t: 0, fired: 0, targets };
|
|
4132
|
+
playing.push(pl);
|
|
4133
|
+
applyClip(pl);
|
|
4134
|
+
action("animation", t("act.anim_start", { name: e.name, clip: clip.id, seconds: num(clip.duration, 1) }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { clip: clip.id, duration: clip.duration } });
|
|
4135
|
+
return true;
|
|
4136
|
+
};
|
|
4137
|
+
const extraScenarios = [...options.scenarios ?? []];
|
|
4138
|
+
const moduleCheck = (ok, text, data, itemId) => {
|
|
4139
|
+
log.add({ channel: "check", type: ok ? "pass" : "fail", text: `${ok ? "\u2713" : "\u2717"} ${text}`, itemId, data: { ok, ...data } });
|
|
4140
|
+
};
|
|
4141
|
+
const loadModule = (mod) => {
|
|
4142
|
+
const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
|
|
4143
|
+
const fail = (problem) => {
|
|
4144
|
+
report.ok = false;
|
|
4145
|
+
report.problems.push(problem);
|
|
4146
|
+
};
|
|
4147
|
+
const added = mod.items(locale);
|
|
4148
|
+
for (const ti of added) {
|
|
4149
|
+
const item = {
|
|
4150
|
+
id: ti.id,
|
|
4151
|
+
name: ti.name,
|
|
4152
|
+
group: mod.group,
|
|
4153
|
+
build: (T) => ti.build(T),
|
|
4154
|
+
spec: ti.spec,
|
|
4155
|
+
movable: ti.movable,
|
|
4156
|
+
fluid: ti.fluid,
|
|
4157
|
+
weapon: ti.weapon,
|
|
4158
|
+
impl: ti.impl,
|
|
4159
|
+
animations: ti.animations
|
|
4160
|
+
};
|
|
4161
|
+
catalogMap.set(item.id, item);
|
|
4162
|
+
const at = catalog.findIndex((c) => c.id === item.id);
|
|
4163
|
+
if (at >= 0) catalog[at] = item;
|
|
4164
|
+
else catalog.push(item);
|
|
4165
|
+
report.items.push(item.id);
|
|
4166
|
+
let partIds = [];
|
|
4167
|
+
const base = { module: mod.id, item: item.id };
|
|
4168
|
+
try {
|
|
4169
|
+
const root = item.build(THREE);
|
|
4170
|
+
root.updateMatrixWorld(true);
|
|
4171
|
+
if (item.impl) {
|
|
4172
|
+
const probe = item.impl(root, `${item.id}_check`);
|
|
4173
|
+
const r = checkDestructible(probe, { materials: world.materials.keys() });
|
|
4174
|
+
partIds = probe.describe().parts.map((p) => p.id);
|
|
4175
|
+
moduleCheck(r.ok, r.ok ? t("mod.destructible_ok", { name: item.name, parts: partIds.length }) : t("mod.destructible_fail", { name: item.name, problems: r.problems.join("; ") }), { ...base, check: "destructible", problems: r.problems }, item.id);
|
|
4176
|
+
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
4177
|
+
} else {
|
|
4178
|
+
partIds = resolveSpec(item, root).spec.parts.map((p) => p.id);
|
|
4179
|
+
}
|
|
4180
|
+
} catch (err) {
|
|
4181
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4182
|
+
moduleCheck(false, t("mod.destructible_fail", { name: item.name, problems: msg }), { ...base, check: "destructible", problems: [msg] }, item.id);
|
|
4183
|
+
fail(`${item.id}: ${msg}`);
|
|
4184
|
+
}
|
|
4185
|
+
for (const clip of item.animations ?? []) {
|
|
4186
|
+
const r = checkAnimation(clip, partIds);
|
|
4187
|
+
moduleCheck(r.ok, r.ok ? t("mod.animation_ok", { name: item.name, clip: clip.id, seconds: num(clip.duration, 1) }) : t("mod.animation_fail", { name: item.name, clip: clip.id, problems: r.problems.join("; ") }), { ...base, check: "animation", clip: clip.id, problems: r.problems }, item.id);
|
|
4188
|
+
for (const p of r.problems) fail(`${item.id}: ${p}`);
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
for (const ms of mod.scenarios?.(locale) ?? []) {
|
|
4192
|
+
const sc = { id: ms.id, name: ms.name, run: ms.run };
|
|
4193
|
+
const at = extraScenarios.findIndex((x) => x.id === sc.id);
|
|
4194
|
+
if (at >= 0) extraScenarios[at] = sc;
|
|
4195
|
+
else extraScenarios.push(sc);
|
|
4196
|
+
report.scenarios.push(sc.id);
|
|
4197
|
+
}
|
|
4198
|
+
action("module", t("act.module_loaded", { name: mod.name, items: report.items.length, scenarios: report.scenarios.length, result: t(report.ok ? "mod.all_ok" : "mod.some_failed") }), { data: { module: mod.id, items: report.items, scenarios: report.scenarios, ok: report.ok } });
|
|
4199
|
+
ui?.refresh();
|
|
4200
|
+
return report;
|
|
4201
|
+
};
|
|
4202
|
+
const allScenarios = () => [...makeScenarios(t, num, locale), ...extraScenarios];
|
|
4203
|
+
let scenarioRunning = false;
|
|
4204
|
+
const step = (seconds) => {
|
|
4205
|
+
const dt = seconds > 6 ? 1 / 30 : FIXED_DT;
|
|
4206
|
+
const n = Math.max(1, Math.round(seconds / dt));
|
|
4207
|
+
for (let i = 0; i < n; i++) {
|
|
4208
|
+
player.update(0);
|
|
4209
|
+
followHeld(dt);
|
|
4210
|
+
stepSim(dt);
|
|
4211
|
+
}
|
|
4212
|
+
};
|
|
4213
|
+
const runScenarios = async (list) => {
|
|
4214
|
+
const all = list ?? allScenarios();
|
|
4215
|
+
const report = { passed: 0, failed: 0, results: [] };
|
|
4216
|
+
scenarioRunning = true;
|
|
4217
|
+
try {
|
|
4218
|
+
for (const sc of all) {
|
|
4219
|
+
hardReset();
|
|
4220
|
+
player.look(arena.playerStart, [0, 1.5, arena.wallZ]);
|
|
4221
|
+
useHeld = false;
|
|
4222
|
+
const checks = [];
|
|
4223
|
+
log.add({ channel: "check", type: "scenario", text: t("sc.start", { name: sc.name }), data: { id: sc.id } });
|
|
4224
|
+
const ctx = {
|
|
4225
|
+
room,
|
|
4226
|
+
step,
|
|
4227
|
+
expect(condition, label, data) {
|
|
4228
|
+
checks.push({ label, ok: !!condition });
|
|
4229
|
+
log.add({ channel: "check", type: condition ? "pass" : "fail", text: `${condition ? "\u2713" : "\u2717"} ${label}`, data: { scenario: sc.id, ok: !!condition, ...data ?? {} } });
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
try {
|
|
4233
|
+
await sc.run(ctx);
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4236
|
+
ctx.expect(false, t("sc.error", { msg }));
|
|
4237
|
+
}
|
|
4238
|
+
flush();
|
|
4239
|
+
const ok = checks.length > 0 && checks.every((c) => c.ok);
|
|
4240
|
+
report.results.push({ id: sc.id, name: sc.name, ok, checks });
|
|
4241
|
+
if (ok) report.passed++;
|
|
4242
|
+
else report.failed++;
|
|
4243
|
+
log.add({ channel: "check", type: ok ? "scenario_pass" : "scenario_fail", text: t(ok ? "sc.pass" : "sc.fail", { name: sc.name }), data: { id: sc.id, ok } });
|
|
4244
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
4245
|
+
}
|
|
4246
|
+
} finally {
|
|
4247
|
+
scenarioRunning = false;
|
|
4248
|
+
}
|
|
4249
|
+
return report;
|
|
4250
|
+
};
|
|
4251
|
+
const room = {
|
|
4252
|
+
world,
|
|
4253
|
+
renderer,
|
|
4254
|
+
scene,
|
|
4255
|
+
camera,
|
|
4256
|
+
arsenal,
|
|
4257
|
+
fluids,
|
|
4258
|
+
fluidRenderer: fluidView,
|
|
4259
|
+
log,
|
|
4260
|
+
get tool() {
|
|
4261
|
+
return tool;
|
|
4262
|
+
},
|
|
4263
|
+
get scenarios() {
|
|
4264
|
+
return allScenarios();
|
|
4265
|
+
},
|
|
4266
|
+
spawn,
|
|
4267
|
+
clear,
|
|
4268
|
+
resetAll,
|
|
4269
|
+
setTool,
|
|
4270
|
+
useItem,
|
|
4271
|
+
use(hold = 0) {
|
|
4272
|
+
if (tool.def.category === "hand") {
|
|
4273
|
+
grab();
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
4276
|
+
useTool(FIXED_DT, false);
|
|
4277
|
+
flush();
|
|
4278
|
+
let t0 = 0;
|
|
4279
|
+
while (t0 < hold) {
|
|
4280
|
+
useTool(FIXED_DT, true);
|
|
4281
|
+
stepSim(FIXED_DT);
|
|
4282
|
+
t0 += FIXED_DT;
|
|
4283
|
+
}
|
|
4284
|
+
tool.release();
|
|
4285
|
+
},
|
|
4286
|
+
grab,
|
|
4287
|
+
throwHeld,
|
|
4288
|
+
look(position, target) {
|
|
4289
|
+
player.look(position, target);
|
|
4290
|
+
},
|
|
4291
|
+
inspect() {
|
|
4292
|
+
const r = inspectTarget();
|
|
4293
|
+
if (!r) return null;
|
|
4294
|
+
return { objectId: r.object, partId: r.part, itemId: entryOf(r.object)?.so.itemId ?? "" };
|
|
4295
|
+
},
|
|
4296
|
+
inspectInfo,
|
|
4297
|
+
spawned() {
|
|
4298
|
+
return [...entries.values()].filter((e) => !e.arena).map((e) => e.so);
|
|
4299
|
+
},
|
|
4300
|
+
update,
|
|
4301
|
+
on(type, fn) {
|
|
4302
|
+
listeners[type].add(fn);
|
|
4303
|
+
return () => listeners[type].delete(fn);
|
|
4304
|
+
},
|
|
4305
|
+
runScenarios,
|
|
4306
|
+
tilt(rad) {
|
|
4307
|
+
heldTilt = rad;
|
|
4308
|
+
},
|
|
4309
|
+
toggleTap,
|
|
4310
|
+
setSlowMotion(on) {
|
|
4311
|
+
timeScale = on ? 0.25 : 1;
|
|
4312
|
+
},
|
|
4313
|
+
setFastForward(on) {
|
|
4314
|
+
fastForward = on;
|
|
4315
|
+
},
|
|
4316
|
+
equip,
|
|
4317
|
+
loadModule,
|
|
4318
|
+
animations: animationsOf,
|
|
4319
|
+
playAnimation,
|
|
4320
|
+
dispose() {
|
|
4321
|
+
cancelAnimationFrame?.(raf);
|
|
4322
|
+
window.removeEventListener("resize", onResize);
|
|
4323
|
+
player.dispose();
|
|
4324
|
+
ui?.dispose();
|
|
4325
|
+
renderer.dispose();
|
|
4326
|
+
fluidView.dispose();
|
|
4327
|
+
for (const tr of tracers) tr.line.removeFromParent();
|
|
4328
|
+
clearProjectileMeshes();
|
|
4329
|
+
if (ownRenderer) {
|
|
4330
|
+
gl.dispose();
|
|
4331
|
+
dom.remove();
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
let ui = null;
|
|
4336
|
+
if (options.ui !== false) {
|
|
4337
|
+
const groups = () => {
|
|
4338
|
+
const m = /* @__PURE__ */ new Map();
|
|
4339
|
+
for (const i of catalog) {
|
|
4340
|
+
const g = m.get(i.group) ?? [];
|
|
4341
|
+
g.push(i);
|
|
4342
|
+
m.set(i.group, g);
|
|
4343
|
+
}
|
|
4344
|
+
return m;
|
|
4345
|
+
};
|
|
4346
|
+
ui = createUi({
|
|
4347
|
+
container,
|
|
4348
|
+
strings: S,
|
|
4349
|
+
t,
|
|
4350
|
+
log,
|
|
4351
|
+
groups,
|
|
4352
|
+
loadout: () => loadout.map((id) => ({ id, name: arsenal.name(id) })),
|
|
4353
|
+
onSpawn: (id) => {
|
|
4354
|
+
spawn(id);
|
|
4355
|
+
},
|
|
4356
|
+
onEquip: (itemId) => {
|
|
4357
|
+
const w = catalogMap.get(itemId)?.weapon;
|
|
4358
|
+
if (w) equip(w);
|
|
4359
|
+
},
|
|
4360
|
+
onClear: clear,
|
|
4361
|
+
onResetAll: resetAll,
|
|
4362
|
+
onSlow: (on) => room.setSlowMotion(on),
|
|
4363
|
+
onFast: (on) => room.setFastForward(on),
|
|
4364
|
+
onQuality: (q) => {
|
|
4365
|
+
qualityName = q;
|
|
4366
|
+
if (ownRenderer) {
|
|
4367
|
+
gl.shadowMap.enabled = q !== "low";
|
|
4368
|
+
gl.setPixelRatio(q === "low" ? 1 : Math.min(typeof devicePixelRatio === "number" ? devicePixelRatio : 1, 2));
|
|
4369
|
+
arena.sun.shadow.mapSize.set(q === "high" ? 2048 : 1024, q === "high" ? 2048 : 1024);
|
|
4370
|
+
arena.sun.shadow.map?.dispose();
|
|
4371
|
+
arena.sun.shadow.map = null;
|
|
4372
|
+
resize();
|
|
4373
|
+
}
|
|
4374
|
+
},
|
|
4375
|
+
quality: qualityName,
|
|
4376
|
+
scenarios: allScenarios,
|
|
4377
|
+
onRun: async (sc) => {
|
|
4378
|
+
await runScenarios([sc]);
|
|
4379
|
+
},
|
|
4380
|
+
onRunAll: async () => {
|
|
4381
|
+
await runScenarios();
|
|
4382
|
+
},
|
|
4383
|
+
onTool: (index) => {
|
|
4384
|
+
toolIndex = index;
|
|
4385
|
+
setTool(loadout[index]);
|
|
4386
|
+
},
|
|
4387
|
+
onTap: () => toggleTap(),
|
|
4388
|
+
onPlay: (objectId, clipId) => {
|
|
4389
|
+
playAnimation(clipId, objectId);
|
|
4390
|
+
},
|
|
4391
|
+
onResize: resize
|
|
4392
|
+
});
|
|
4393
|
+
ui.hud(hudState());
|
|
4394
|
+
}
|
|
4395
|
+
let raf = 0;
|
|
4396
|
+
if (ownRenderer && typeof requestAnimationFrame === "function") {
|
|
4397
|
+
let last = performance.now();
|
|
4398
|
+
const loop = (now) => {
|
|
4399
|
+
raf = requestAnimationFrame(loop);
|
|
4400
|
+
const dt = Math.min(0.05, (now - last) / 1e3);
|
|
4401
|
+
last = now;
|
|
4402
|
+
if (!scenarioRunning) update(dt);
|
|
4403
|
+
gl.render(scene, camera);
|
|
4404
|
+
};
|
|
4405
|
+
raf = requestAnimationFrame(loop);
|
|
4406
|
+
}
|
|
4407
|
+
if (options.initial) {
|
|
4408
|
+
for (const [i, id] of options.initial.entries()) spawn(id, [(i - (options.initial.length - 1) / 2) * 1.3, 0, 0.5]);
|
|
4409
|
+
}
|
|
4410
|
+
return room;
|
|
4411
|
+
}
|
|
4412
|
+
function structuredCloneSpec(spec) {
|
|
4413
|
+
return JSON.parse(JSON.stringify(spec));
|
|
4414
|
+
}
|
|
4415
|
+
export {
|
|
4416
|
+
DEMO_ITEMS,
|
|
4417
|
+
DEMO_MATERIALS,
|
|
4418
|
+
MATERIAL_COLORS,
|
|
4419
|
+
SCENARIOS,
|
|
4420
|
+
brickWallSpec,
|
|
4421
|
+
buildFromSpec,
|
|
4422
|
+
createLog,
|
|
4423
|
+
createTestRoom,
|
|
4424
|
+
demoItems,
|
|
4425
|
+
locales,
|
|
4426
|
+
makeScenarios,
|
|
4427
|
+
resolveStrings,
|
|
4428
|
+
scenariosFor
|
|
4429
|
+
};
|
|
4430
|
+
//# sourceMappingURL=index.js.map
|